@logictan/dsh-config-manager 0.1.60
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/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原子写 primitive 测试(Phase 1)。
|
|
3
|
+
* - Basic:新文件/替换/Unicode/空/大文件/嵌套目录/mode 保留/敏感 open-wx-0600
|
|
4
|
+
* - 故障注入:AtomicIo 逐点失败 → 断言「target 要么旧完整、要么新完整」
|
|
5
|
+
* - 并发:双写同 target 终态完整
|
|
6
|
+
* - Symlink/Hardlink:follow / reject / hardlink 断开无损坏
|
|
7
|
+
* - Windows:rename-overwrite、EPERM 注入重试+失败不改 target
|
|
8
|
+
* - 崩溃模拟:子进程 tmp 写后、rename 后 exit → 磁盘完整性
|
|
9
|
+
*/
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import fssync from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import {
|
|
18
|
+
atomicWriteFile,
|
|
19
|
+
atomicWriteFileSync,
|
|
20
|
+
atomicCopyFile,
|
|
21
|
+
sanitizeTmpBase,
|
|
22
|
+
type AtomicIo,
|
|
23
|
+
type AtomicHandle,
|
|
24
|
+
} from './atomic-write.ts';
|
|
25
|
+
|
|
26
|
+
function tmp(t: test.TestContext): string {
|
|
27
|
+
const dir = fssync.mkdtempSync(path.join(os.tmpdir(), 'atomic-write-'));
|
|
28
|
+
t.after(() => fssync.rmSync(dir, { recursive: true, force: true }));
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------- 可注入失败点的 AtomicIo ----------
|
|
33
|
+
// 基于真实 node:fs/promises,仅在某些步骤抛错;open 返回真实 handle 包装。
|
|
34
|
+
function failingIo(): AtomicIo & { failAt(step: string): void } {
|
|
35
|
+
const failSet = new Set<string>();
|
|
36
|
+
const maybeThrow = (step: string) => {
|
|
37
|
+
if (failSet.has(step)) throw new Error(`injected failure at ${step}`);
|
|
38
|
+
};
|
|
39
|
+
const io: AtomicIo = {
|
|
40
|
+
async mkdir(d, o) { maybeThrow('mkdir'); await fs.mkdir(d, o); },
|
|
41
|
+
async lstat(p) {
|
|
42
|
+
maybeThrow('lstat');
|
|
43
|
+
try { return await fs.lstat(p); } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; throw e; }
|
|
44
|
+
},
|
|
45
|
+
async stat(p) {
|
|
46
|
+
maybeThrow('stat');
|
|
47
|
+
try { return await fs.stat(p); } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; throw e; }
|
|
48
|
+
},
|
|
49
|
+
async realpath(p) { maybeThrow('realpath'); return fs.realpath(p); },
|
|
50
|
+
async open(p, flag, mode) {
|
|
51
|
+
maybeThrow('open');
|
|
52
|
+
const handle = await fs.open(p, flag as 'wx', mode);
|
|
53
|
+
return wrap(handle);
|
|
54
|
+
},
|
|
55
|
+
async rename(a, b) { maybeThrow('rename'); await fs.rename(a, b); },
|
|
56
|
+
async unlink(p) { maybeThrow('unlink'); await fs.unlink(p); },
|
|
57
|
+
async copyFile(a, b) { maybeThrow('copyFile'); await fs.copyFile(a, b); },
|
|
58
|
+
async chmod(p, m) { maybeThrow('chmod'); await fs.chmod(p, m); },
|
|
59
|
+
async fsyncDir() { maybeThrow('fsyncDir'); },
|
|
60
|
+
async sleep(ms) { maybeThrow('sleep'); await new Promise((r) => setTimeout(r, ms)); },
|
|
61
|
+
};
|
|
62
|
+
const wrap = (h: Awaited<ReturnType<typeof fs.open>>): AtomicHandle => ({
|
|
63
|
+
async writeFile(data) { maybeThrow('write'); await h.writeFile(data); },
|
|
64
|
+
async sync() { maybeThrow('sync'); await h.sync(); },
|
|
65
|
+
async close() { maybeThrow('close'); await h.close(); },
|
|
66
|
+
});
|
|
67
|
+
return { ...io, failAt: (s) => { failSet.add(s); } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readText(p: string): Promise<string> { return new TextDecoder().decode(await fs.readFile(p)); }
|
|
71
|
+
|
|
72
|
+
test('sanitizeTmpBase 拒绝危险文件名', () => {
|
|
73
|
+
assert.equal(sanitizeTmpBase('settings.yaml'), 'settings.yaml');
|
|
74
|
+
assert.throws(() => sanitizeTmpBase('..'));
|
|
75
|
+
assert.throws(() => sanitizeTmpBase('a/b'));
|
|
76
|
+
assert.throws(() => sanitizeTmpBase('a\\b'));
|
|
77
|
+
assert.throws(() => sanitizeTmpBase(''));
|
|
78
|
+
assert.throws(() => sanitizeTmpBase('a:b'));
|
|
79
|
+
assert.throws(() => sanitizeTmpBase('a\u0000b'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('Basic:新文件(嵌套目录自动创建)', async (t) => {
|
|
83
|
+
const dir = tmp(t);
|
|
84
|
+
const p = path.join(dir, 'a', 'b', 'new.yaml');
|
|
85
|
+
await atomicWriteFile(p, new TextEncoder().encode('k: v\n'));
|
|
86
|
+
assert.equal(await readText(p), 'k: v\n');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Basic:替换已有文件', async (t) => {
|
|
90
|
+
const dir = tmp(t);
|
|
91
|
+
const p = path.join(dir, 'x.json');
|
|
92
|
+
await fs.writeFile(p, '{"old":true}');
|
|
93
|
+
await atomicWriteFile(p, new TextEncoder().encode('{"new":true}'));
|
|
94
|
+
assert.equal(await readText(p), '{"new":true}');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('Basic:Unicode 路径', async (t) => {
|
|
98
|
+
const dir = tmp(t);
|
|
99
|
+
const p = path.join(dir, '配置', '设置-🎯.yaml');
|
|
100
|
+
await atomicWriteFile(p, new TextEncoder().encode('ok'));
|
|
101
|
+
assert.equal(await readText(p), 'ok');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('Basic:空文件', async (t) => {
|
|
105
|
+
const dir = tmp(t);
|
|
106
|
+
const p = path.join(dir, 'empty.txt');
|
|
107
|
+
await atomicWriteFile(p, new Uint8Array(0));
|
|
108
|
+
assert.equal(await readText(p), '');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('Basic:大文件(1.5MB)', async (t) => {
|
|
112
|
+
const dir = tmp(t);
|
|
113
|
+
const p = path.join(dir, 'big.bin');
|
|
114
|
+
const data = new Uint8Array(1_500_000).fill(0x41);
|
|
115
|
+
await atomicWriteFile(p, data);
|
|
116
|
+
const read = await fs.readFile(p);
|
|
117
|
+
assert.equal(read.length, data.length);
|
|
118
|
+
assert.ok(read.equals(Buffer.from(data)));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('Basic:mode 保留(POSIX 0o700 继承;Windows 仅验证内容)', async (t) => {
|
|
122
|
+
const dir = tmp(t);
|
|
123
|
+
const p = path.join(dir, 'cfg.json');
|
|
124
|
+
await fs.writeFile(p, 'old', { mode: 0o700 });
|
|
125
|
+
await atomicWriteFile(p, new TextEncoder().encode('new'));
|
|
126
|
+
if (process.platform === 'win32') {
|
|
127
|
+
// Windows 对 POSIX 权限位不生效(设计 §2.4 已声明 mode 语义弱):只验证内容与可读
|
|
128
|
+
assert.equal(await readText(p), 'new');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
assert.equal((await fs.stat(p)).mode & 0o777, 0o700);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('Basic:sensitive 新文件 open-wx-0600(POSIX 断言 0600;Windows 验证内容)', async (t) => {
|
|
135
|
+
const dir = tmp(t);
|
|
136
|
+
const p = path.join(dir, 'secrets.env');
|
|
137
|
+
await atomicWriteFile(p, new TextEncoder().encode('TOKEN=x'), { mode: 0o600 });
|
|
138
|
+
if (process.platform === 'win32') {
|
|
139
|
+
assert.equal(await readText(p), 'TOKEN=x');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
assert.equal((await fs.stat(p)).mode & 0o777, 0o600);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('故障注入:write 失败 → target 保持旧完整,无 tmp 残留', async (t) => {
|
|
146
|
+
const dir = tmp(t);
|
|
147
|
+
const p = path.join(dir, 'f.json');
|
|
148
|
+
await fs.writeFile(p, 'old-complete');
|
|
149
|
+
const io = failingIo();
|
|
150
|
+
io.failAt('write');
|
|
151
|
+
await assert.rejects(() => atomicWriteFile(p, new TextEncoder().encode('new'), { io }));
|
|
152
|
+
assert.equal(await readText(p), 'old-complete');
|
|
153
|
+
assert.equal((await fs.readdir(dir)).filter((n) => n.startsWith('.dshcm.')).length, 0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('故障注入:sync 失败 → target 保持旧完整 + tmp 清理', async (t) => {
|
|
157
|
+
const dir = tmp(t);
|
|
158
|
+
const p = path.join(dir, 'f.json');
|
|
159
|
+
await fs.writeFile(p, 'old-complete');
|
|
160
|
+
const io = failingIo();
|
|
161
|
+
io.failAt('sync');
|
|
162
|
+
await assert.rejects(() => atomicWriteFile(p, new TextEncoder().encode('new'), { io }));
|
|
163
|
+
assert.equal(await readText(p), 'old-complete');
|
|
164
|
+
assert.equal((await fs.readdir(dir)).filter((n) => n.startsWith('.dshcm.')).length, 0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('故障注入:rename 持续失败 → 不改写原 target,且 tmp 已清理', async (t) => {
|
|
168
|
+
const dir = tmp(t);
|
|
169
|
+
const p = path.join(dir, 'f.json');
|
|
170
|
+
await fs.writeFile(p, 'old-complete');
|
|
171
|
+
const io = failingIo();
|
|
172
|
+
io.failAt('rename');
|
|
173
|
+
await assert.rejects(() => atomicWriteFile(p, new TextEncoder().encode('new'), { io, retryDelayMs: 1 }));
|
|
174
|
+
assert.equal(await readText(p), 'old-complete');
|
|
175
|
+
// rename 失败后尽力清理 tmp(不残留 orphan)
|
|
176
|
+
assert.equal((await fs.readdir(dir)).filter((n) => n.startsWith('.dshcm.')).length, 0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('故障注入:EPERM 一次后重试成功(有界重试恢复)', async (t) => {
|
|
180
|
+
const dir = tmp(t);
|
|
181
|
+
const p = path.join(dir, 'f.json');
|
|
182
|
+
await fs.writeFile(p, 'old-complete');
|
|
183
|
+
let renameCalls = 0;
|
|
184
|
+
const io = failingIo();
|
|
185
|
+
const counted: AtomicIo = {
|
|
186
|
+
...io,
|
|
187
|
+
async rename(a, b) {
|
|
188
|
+
renameCalls++;
|
|
189
|
+
if (renameCalls === 1) { const e: NodeJS.ErrnoException = new Error('injected'); e.code = 'EBUSY'; throw e; }
|
|
190
|
+
await fs.rename(a, b);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
await atomicWriteFile(p, new TextEncoder().encode('new'), { io: counted, retryDelayMs: 1 });
|
|
194
|
+
assert.equal(renameCalls, 2);
|
|
195
|
+
assert.equal(await readText(p), 'new');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('故障注入:unlink(tmp) 失败 → onCleanupFailure 留痕,不吞错', async (t) => {
|
|
199
|
+
const dir = tmp(t);
|
|
200
|
+
const p = path.join(dir, 'f.json');
|
|
201
|
+
await fs.writeFile(p, 'old-complete');
|
|
202
|
+
const io = failingIo();
|
|
203
|
+
io.failAt('write');
|
|
204
|
+
io.failAt('unlink');
|
|
205
|
+
const cleaned: string[] = [];
|
|
206
|
+
await assert.rejects(() => atomicWriteFile(p, new TextEncoder().encode('new'), { io, onCleanupFailure: (tp) => cleaned.push(tp) }));
|
|
207
|
+
assert.ok(cleaned.length > 0);
|
|
208
|
+
assert.equal(await readText(p), 'old-complete');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('并发:双写同 target 终态为完整 A 或完整 B', async (t) => {
|
|
212
|
+
const dir = tmp(t);
|
|
213
|
+
const p = path.join(dir, 'c.json');
|
|
214
|
+
const a = new Uint8Array(500_000).fill(0x41);
|
|
215
|
+
const b = new Uint8Array(500_000).fill(0x42);
|
|
216
|
+
await Promise.all([atomicWriteFile(p, a), atomicWriteFile(p, b)]);
|
|
217
|
+
const read = await fs.readFile(p);
|
|
218
|
+
assert.equal(read.length, 500_000);
|
|
219
|
+
const allA = read.every((byte) => byte === 0x41);
|
|
220
|
+
const allB = read.every((byte) => byte === 0x42);
|
|
221
|
+
assert.ok(allA || allB, '终态必须为完整 A 或完整 B');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('Symlink follow:写真实目标,symlink 结构保留', async (t) => {
|
|
225
|
+
const dir = tmp(t);
|
|
226
|
+
const real = path.join(dir, 'real.yaml');
|
|
227
|
+
const link = path.join(dir, 'link.yaml');
|
|
228
|
+
await fs.writeFile(real, 'old');
|
|
229
|
+
try { fssync.symlinkSync(real, link); } catch { t.diagnostic('symlink 创建失败,跳过'); return; }
|
|
230
|
+
await atomicWriteFile(link, new TextEncoder().encode('new'));
|
|
231
|
+
assert.equal(await readText(real), 'new');
|
|
232
|
+
assert.ok((await fs.lstat(link)).isSymbolicLink());
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('Symlink reject:拒绝写 symlink,不改真实目标', async (t) => {
|
|
236
|
+
const dir = tmp(t);
|
|
237
|
+
const real = path.join(dir, 'real.env');
|
|
238
|
+
const link = path.join(dir, 'link.env');
|
|
239
|
+
await fs.writeFile(real, 'old-secret');
|
|
240
|
+
try { fssync.symlinkSync(real, link); } catch { t.diagnostic('symlink 创建失败,跳过'); return; }
|
|
241
|
+
await assert.rejects(() => atomicWriteFile(link, new TextEncoder().encode('evil'), { symlink: 'reject' }));
|
|
242
|
+
assert.equal(await readText(real), 'old-secret');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('Hardlink:原子替换断开 hardlink(兄弟指向旧 inode,无损坏)', async (t) => {
|
|
246
|
+
const dir = tmp(t);
|
|
247
|
+
const a = path.join(dir, 'a.txt');
|
|
248
|
+
const b = path.join(dir, 'b.txt');
|
|
249
|
+
await fs.writeFile(a, 'x');
|
|
250
|
+
try { fssync.linkSync(a, b); } catch { t.diagnostic('hardlink 创建失败,跳过'); return; }
|
|
251
|
+
await atomicWriteFile(a, new TextEncoder().encode('new-a'));
|
|
252
|
+
assert.equal(await readText(a), 'new-a');
|
|
253
|
+
const bContent = await readText(b);
|
|
254
|
+
// 允许兄弟指向旧内容(断开),但不得损坏/半写
|
|
255
|
+
assert.ok(bContent === 'x' || bContent === 'new-a');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('atomicWriteFileSync:同步版新建+替换', (t) => {
|
|
259
|
+
const dir = tmp(t);
|
|
260
|
+
const p = path.join(dir, 's.json');
|
|
261
|
+
atomicWriteFileSync(p, new TextEncoder().encode('one'));
|
|
262
|
+
assert.equal(fssync.readFileSync(p, 'utf8'), 'one');
|
|
263
|
+
atomicWriteFileSync(p, new TextEncoder().encode('two'));
|
|
264
|
+
assert.equal(fssync.readFileSync(p, 'utf8'), 'two');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('atomicCopyFile:大文件流式拷贝 + 覆盖已存在', async (t) => {
|
|
268
|
+
const dir = tmp(t);
|
|
269
|
+
const src = path.join(dir, 'src.big');
|
|
270
|
+
const dst = path.join(dir, 'dst.big');
|
|
271
|
+
const data = new Uint8Array(2_500_000).fill(0x33);
|
|
272
|
+
await fs.writeFile(src, data);
|
|
273
|
+
await atomicCopyFile(src, dst);
|
|
274
|
+
assert.ok((await fs.readFile(dst)).equals(Buffer.from(data)));
|
|
275
|
+
await fs.writeFile(dst, 'short');
|
|
276
|
+
await atomicCopyFile(src, dst);
|
|
277
|
+
assert.ok((await fs.readFile(dst)).equals(Buffer.from(data)));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('Windows:rename-overwrite 已存在(本机即 Windows 实跑)', async (t) => {
|
|
281
|
+
const dir = tmp(t);
|
|
282
|
+
const p = path.join(dir, 'w.txt');
|
|
283
|
+
await fs.writeFile(p, 'old');
|
|
284
|
+
await atomicWriteFile(p, new TextEncoder().encode('new'));
|
|
285
|
+
assert.equal(await readText(p), 'new');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('崩溃模拟:tmp 写+fsync 后 rename 前 exit → target 保持旧完整', async (t) => {
|
|
289
|
+
const dir = tmp(t);
|
|
290
|
+
const target = path.join(dir, 'crash.json');
|
|
291
|
+
const childScript = `
|
|
292
|
+
const fsr = require('node:fs/promises'); const path = require('node:path');
|
|
293
|
+
(async () => {
|
|
294
|
+
const dir = ${JSON.stringify(dir)}; const target = ${JSON.stringify(target)};
|
|
295
|
+
await fsr.writeFile(target, 'old-complete');
|
|
296
|
+
const tmp = path.join(dir, '.dshcm.crash.' + process.pid + '.deadbeef.tmp');
|
|
297
|
+
const fh = await fsr.open(tmp, 'wx');
|
|
298
|
+
await fh.writeFile('new-content'); await fh.sync(); await fh.close();
|
|
299
|
+
process.exit(42); // 模拟崩溃:tmp 已写完整,未 rename
|
|
300
|
+
})();
|
|
301
|
+
`;
|
|
302
|
+
try {
|
|
303
|
+
execFileSync(process.execPath, ['-e', childScript], { encoding: 'utf8' });
|
|
304
|
+
assert.fail('子进程应以非零码退出');
|
|
305
|
+
} catch (e) {
|
|
306
|
+
assert.equal((e as { status?: number }).status, 42);
|
|
307
|
+
}
|
|
308
|
+
// target 未被改动(仍是旧完整版);孤儿 tmp 可被识别(.dshcm. 前缀 + 死 pid)
|
|
309
|
+
assert.equal(await readText(target), 'old-complete');
|
|
310
|
+
const orphans = (await fs.readdir(dir)).filter((n) => n.startsWith('.dshcm.'));
|
|
311
|
+
assert.equal(orphans.length, 1, '崩溃后应留下可识别的孤儿 tmp(供 orphan sweep)');
|
|
312
|
+
});
|