@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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 真实 child-process crash 注入(C1-C10)。
|
|
3
|
+
* 每个窗口用独立子进程执行 phase4-crash-child.ts(真实 runJournaled deferred + 真实 FileSnapshotStore),
|
|
4
|
+
* 在指定点 SIGKILL,父进程随后用真实 JournalStore/reconcile 判定:
|
|
5
|
+
* - C1/C2:快照未 READY+绑定前 crash → 无 trusted bound snapshot,recovery required / 保守
|
|
6
|
+
* - C3:READY snapshot 已绑定(SNAPSHOT_CREATED)但未 APPLYING → 无 destructive mutation,安全
|
|
7
|
+
* - C5/C6/C7:partial/local file mutation 已发生 + trusted snapshot 绑定 → 绝不可静默 RECOVERED
|
|
8
|
+
* - C8/C9/C10:业务成功/COMMITTED 后 → 不 rollback
|
|
9
|
+
* 这是 process.kill(SIGKILL) 真实注入,非 throw 替代。
|
|
10
|
+
*/
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import fssync from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { JournalStore } from './journal.ts';
|
|
20
|
+
import { Phase3Recovery } from './phase3-host.ts';
|
|
21
|
+
import { reconcileActive } from './reconcile.ts';
|
|
22
|
+
import { verifySnapshot } from './backup.ts';
|
|
23
|
+
|
|
24
|
+
const here = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const childPath = path.join(here, 'phase4-crash-child.ts');
|
|
26
|
+
|
|
27
|
+
function workdir(t: test.TestContext): string {
|
|
28
|
+
const dir = fssync.mkdtempSync(path.join(os.tmpdir(), 'dsh-cm-phase4-crash-'));
|
|
29
|
+
t.after(() => fssync.rmSync(dir, { recursive: true, force: true }));
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 跑一个 crash 窗口的子进程;返回 { workdir, store, dataDir }(事务可能已完成或残留 active) */
|
|
34
|
+
async function runCrashPoint(t: test.TestContext, point: string): Promise<{ workDir: string; dataDir: string }> {
|
|
35
|
+
const workDir = workdir(t);
|
|
36
|
+
await new Promise<void>((resolve) => {
|
|
37
|
+
const child = spawn(process.execPath, [childPath, workDir, point], { stdio: 'ignore' });
|
|
38
|
+
child.on('close', () => resolve());
|
|
39
|
+
});
|
|
40
|
+
return { workDir, dataDir: path.join(workDir, 'data') };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readJournal(dataDir: string): Promise<import('./journal.ts').OperationJournal | null> {
|
|
44
|
+
const store = new JournalStore({ transactionsDir: path.join(dataDir, 'transactions') });
|
|
45
|
+
await store.ensureDirs();
|
|
46
|
+
const active = await store.scanActive();
|
|
47
|
+
for (const id of active) {
|
|
48
|
+
const j = await store.loadActive(id);
|
|
49
|
+
if (j) return j;
|
|
50
|
+
}
|
|
51
|
+
// 已规整到 completed 的(如 C8-C10 COMMITTED)
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 断言某个 crash 窗口后:journal 状态 + snapshot 完整性 + safe mode */
|
|
56
|
+
async function assertRecovery(
|
|
57
|
+
t: test.TestContext,
|
|
58
|
+
point: string,
|
|
59
|
+
expect: { journalState?: string[]; snapshotReady?: boolean; expectRecovered?: boolean },
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const { workDir, dataDir } = await runCrashPoint(t, point);
|
|
62
|
+
const snapDir = path.join(dataDir, 'snapshots');
|
|
63
|
+
const j = await readJournal(dataDir);
|
|
64
|
+
const txnDir = path.join(dataDir, 'transactions');
|
|
65
|
+
|
|
66
|
+
if (expect.journalState !== undefined) {
|
|
67
|
+
assert.ok(j, `${point}: 应有 journal`);
|
|
68
|
+
assert.ok(
|
|
69
|
+
expect.journalState.includes(j!.state),
|
|
70
|
+
`${point}: journal state=${j!.state} 应为 ${expect.journalState.join('/')}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let snapReady = false;
|
|
75
|
+
if (j?.snapshotId) {
|
|
76
|
+
const v = await verifySnapshot(snapDir, j.snapshotId);
|
|
77
|
+
snapReady = v.ok;
|
|
78
|
+
}
|
|
79
|
+
assert.equal(snapReady, expect.snapshotReady ?? false, `${point}: snapshot READY=${snapReady} 期望 ${expect.snapshotReady}`);
|
|
80
|
+
|
|
81
|
+
// 真实 reconcile:保守 hooks(生产 startup 同强度)
|
|
82
|
+
const store = new JournalStore({ transactionsDir: txnDir });
|
|
83
|
+
const out = await reconcileActive(store, {
|
|
84
|
+
verifyStepFingerprint: async () => 'unable',
|
|
85
|
+
probeExternal: async () => 'unknown',
|
|
86
|
+
snapshotExists: async (id) => (id !== null ? (await verifySnapshot(snapDir, id)).ok : false),
|
|
87
|
+
}, { environmentFingerprint: 'fp-crash', isLiveOwner: async () => false });
|
|
88
|
+
|
|
89
|
+
const decisionKind = out.decisions[0]?.kind;
|
|
90
|
+
if (expect.expectRecovered === true) {
|
|
91
|
+
assert.equal(decisionKind, 'recovered', `${point}: 期望 recovered,实际 ${decisionKind}`);
|
|
92
|
+
} else if (expect.expectRecovered === false) {
|
|
93
|
+
assert.notEqual(decisionKind, 'recovered', `${point}: 绝不可静默 recovered(partial mutation)`);
|
|
94
|
+
assert.ok(
|
|
95
|
+
decisionKind === 'needs-attention' || decisionKind === 'rollback-recommended' || decisionKind === 'noop' || decisionKind === 'rollback-continue',
|
|
96
|
+
`${point}: 保守恢复(${decisionKind})`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test('C1: after journal CREATED, before snapshot → 无 trusted snapshot,reconcile 保守(不 RECOVERED 掩盖部分态)', async (t) => {
|
|
103
|
+
await assertRecovery(t, 'C1', { expectRecovered: false });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('C2: during snapshot write, before bind → 无 trusted bound snapshot,保守', async (t) => {
|
|
107
|
+
await assertRecovery(t, 'C2', { expectRecovered: false });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('C3: READY snapshot 已绑定(SNAPSHOT_CREATED)但未 APPLYING → 无 destructive mutation,安全(non-RECOVERED 保守)', async (t) => {
|
|
111
|
+
await assertRecovery(t, 'C3', { snapshotReady: true, expectRecovered: false });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('C4: after APPLYING, before mutation → trusted snapshot bound,保守(安全)', async (t) => {
|
|
115
|
+
await assertRecovery(t, 'C4', { snapshotReady: true, expectRecovered: false });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('C5/C6/C7: partial/local file mutation + trusted snapshot → 绝不可静默 RECOVERED', async (t) => {
|
|
119
|
+
// C6(mid-mutation 后 local file 已写)是核心:有 trusted bound snapshot 且 local 已变
|
|
120
|
+
for (const point of ['C5', 'C6', 'C7']) {
|
|
121
|
+
await assertRecovery(t, point, { snapshotReady: true, expectRecovered: false });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('C8/C9/C10: 业务成功/COMMITTED 后 → 不 rollback(journal COMMITTED 已规整,无 active 残留)', async (t) => {
|
|
126
|
+
for (const point of ['C8', 'C9', 'C10']) {
|
|
127
|
+
const { dataDir } = await runCrashPoint(t, point);
|
|
128
|
+
const snapDir = path.join(dataDir, 'snapshots');
|
|
129
|
+
const txDir = path.join(dataDir, 'transactions');
|
|
130
|
+
const store = new JournalStore({ transactionsDir: txDir });
|
|
131
|
+
// COMMITTED 后 journal 已规整到 completed → active 无残留 → 不需任何自动 rollback
|
|
132
|
+
const active = await store.scanActive();
|
|
133
|
+
assert.equal(active.length, 0, `${point}: COMMITTED 后不应有 active journal(无需回滚)`);
|
|
134
|
+
// 即便 reconcile 也只会 noop/recovered(不触发 destructive rollback)
|
|
135
|
+
const out = await reconcileActive(store, {
|
|
136
|
+
verifyStepFingerprint: async () => 'unable',
|
|
137
|
+
probeExternal: async () => 'unknown',
|
|
138
|
+
snapshotExists: async (id) => (id !== null ? (await verifySnapshot(snapDir, id)).ok : false),
|
|
139
|
+
}, { environmentFingerprint: 'fp-crash', isLiveOwner: async () => false });
|
|
140
|
+
const kind = out.decisions[0]?.kind;
|
|
141
|
+
assert.notEqual(kind, 'rollback-continue', `${point}: COMMITTED 后不得进入回滚`);
|
|
142
|
+
assert.notEqual(kind, 'needs-attention', `${point}: COMMITTED 后不得 needs-attention`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 明确断言「空 steps + partial mutation 绝不 RECOVERED」(F20 生产回归;对应 C5 后的磁盘态)
|
|
147
|
+
test('C6: local file mutation 后 + trusted snapshot → reconcile snapshotExists 正向 → 推荐回滚而非 RECOVERED', async (t) => {
|
|
148
|
+
const { dataDir } = await runCrashPoint(t, 'C6');
|
|
149
|
+
const snapDir = path.join(dataDir, 'snapshots');
|
|
150
|
+
const txnDir = path.join(dataDir, 'transactions');
|
|
151
|
+
const store = new JournalStore({ transactionsDir: txnDir });
|
|
152
|
+
const j = await readJournal(dataDir);
|
|
153
|
+
assert.ok(j && j.snapshotId, 'C6 后 journal 已绑定 trusted snapshot');
|
|
154
|
+
const snapOk = await verifySnapshot(snapDir, j!.snapshotId!);
|
|
155
|
+
assert.equal(snapOk.ok, true, 'C6 后快照完整(READY+manifest+blob)');
|
|
156
|
+
// 生产 snapshotExists 正向:binding 匹配 → true
|
|
157
|
+
const out = await reconcileActive(store, {
|
|
158
|
+
verifyStepFingerprint: async () => 'unable',
|
|
159
|
+
probeExternal: async () => 'unknown',
|
|
160
|
+
snapshotExists: async (id) => (id !== null ? (await verifySnapshot(snapDir, id)).ok : false),
|
|
161
|
+
}, { environmentFingerprint: 'fp-crash', isLiveOwner: async () => false });
|
|
162
|
+
const kind = out.decisions[0]?.kind;
|
|
163
|
+
assert.notEqual(kind, 'recovered', 'partial mutation 绝不静默 recovered');
|
|
164
|
+
assert.ok(kind === 'needs-attention' || kind === 'rollback-recommended', `应为保守/回滚推荐,实际 ${kind}`);
|
|
165
|
+
assert.equal(out.safeModeRequired, true, 'partial mutation + 不可证明 → SAFE MODE');
|
|
166
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin-cli 文件读取层测试(M5 补写,failing-first 目标:listInstalled 从文件实时读真实版本)。
|
|
3
|
+
* 使用真实临时目录(node:os tmpdir)+ 真实 fs,不 mock 磁盘——验证的是真实读取语义:
|
|
4
|
+
* - readInstalledVersion 读 node_modules/<name>/package.json 落盘版本(不是声明 spec)
|
|
5
|
+
* - readInstalled 过滤 in-box bundles(@deepseek-ai/dsh-base / dsh-web-app / dsh-headless)
|
|
6
|
+
* - listInstalledPlugins 组装 PluginInfo(name/version/isBundle/inBundles)
|
|
7
|
+
* - reconcileBundles 按已装状态维护 dsh.profile.bundles 并写回
|
|
8
|
+
*/
|
|
9
|
+
import test from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
listInstalledPlugins, readInstalled, readInstalledVersion, reconcileBundles,
|
|
17
|
+
resolveProfileDir,
|
|
18
|
+
} from './plugin-cli.ts';
|
|
19
|
+
import type { PluginInfo } from './types.ts';
|
|
20
|
+
|
|
21
|
+
/** 建一个带真实文件的临时 profile(web),返回 { homeDir, profileDir, cleanup }。 */
|
|
22
|
+
function makeTempProfile(deps: Record<string, string>, bundles: string[] = []): {
|
|
23
|
+
homeDir: string;
|
|
24
|
+
profileDir: string;
|
|
25
|
+
cleanup: () => void;
|
|
26
|
+
} {
|
|
27
|
+
const homeDir = mkdtempSync(join(tmpdir(), 'dsh-cm-fs-'));
|
|
28
|
+
const profileDir = resolveProfileDir(homeDir, 'web');
|
|
29
|
+
mkdirSync(profileDir, { recursive: true });
|
|
30
|
+
writeFileSync(
|
|
31
|
+
join(profileDir, 'package.json'),
|
|
32
|
+
`${JSON.stringify({ name: 'dsh-profile-web', dependencies: deps, dsh: { profile: { bundles } } }, null, 2)}\n`,
|
|
33
|
+
'utf8',
|
|
34
|
+
);
|
|
35
|
+
return {
|
|
36
|
+
homeDir,
|
|
37
|
+
profileDir,
|
|
38
|
+
cleanup: () => rmSync(homeDir, { recursive: true, force: true }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 写 node_modules/<name>/package.json。 */
|
|
43
|
+
function writeInstalledPkg(profileDir: string, name: string, version: string, bundlePatch?: string): void {
|
|
44
|
+
const pkgDir = join(profileDir, 'node_modules', name);
|
|
45
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
46
|
+
const manifest: Record<string, unknown> = { name, version };
|
|
47
|
+
if (bundlePatch !== undefined) manifest['dsh'] = { bundle: { patch: bundlePatch } };
|
|
48
|
+
writeFileSync(join(pkgDir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('readInstalledVersion: 读 node_modules 落盘真实版本;未装 / 无 version 字段 → null', () => {
|
|
52
|
+
const { profileDir, cleanup } = makeTempProfile({});
|
|
53
|
+
try {
|
|
54
|
+
writeInstalledPkg(profileDir, 'pkg-a', '0.2.3');
|
|
55
|
+
assert.equal(readInstalledVersion(profileDir, 'pkg-a'), '0.2.3');
|
|
56
|
+
assert.equal(readInstalledVersion(profileDir, 'not-installed'), null, '未装 → null');
|
|
57
|
+
// 落盘 package.json 存在但没有 version 字段 → null(不抛)
|
|
58
|
+
writeInstalledPkg(profileDir, 'broken', 'x');
|
|
59
|
+
writeFileSync(join(profileDir, 'node_modules', 'broken', 'package.json'), '{"name":"broken"}\n', 'utf8');
|
|
60
|
+
assert.equal(readInstalledVersion(profileDir, 'broken'), null);
|
|
61
|
+
} finally {
|
|
62
|
+
cleanup();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('readInstalled: 返回真实依赖映射并过滤 in-box bundles,保留社区插件', () => {
|
|
67
|
+
const { profileDir, cleanup } = makeTempProfile({
|
|
68
|
+
'@deepseek-ai/dsh-base': '0.1.0-rc.6',
|
|
69
|
+
'@deepseek-ai/dsh-web-app': '0.1.0-rc.6',
|
|
70
|
+
'@deepseek-ai/dsh-headless': '0.1.0-rc.6',
|
|
71
|
+
'@linxin666/dsh-ssh': '^0.1.12',
|
|
72
|
+
'pkg-a': '1.0.0',
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
const installed = readInstalled(profileDir);
|
|
76
|
+
assert.deepEqual(installed, { '@linxin666/dsh-ssh': '^0.1.12', 'pkg-a': '1.0.0' });
|
|
77
|
+
} finally {
|
|
78
|
+
cleanup();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('listInstalledPlugins: 版本取 node_modules 真实落盘版本(声明 ^0.1.0 → 实际 0.1.12)', () => {
|
|
83
|
+
// failing-first 目标:listInstalled 从文件实时读真实版本,而非声明 spec。
|
|
84
|
+
const { homeDir, profileDir, cleanup } = makeTempProfile({
|
|
85
|
+
'@linxin666/dsh-ssh': '^0.1.0',
|
|
86
|
+
'pkg-a': '1.0.0',
|
|
87
|
+
'dsh-memory-evolve': 'github:csyangwen/dsh-memory-evolve',
|
|
88
|
+
'@deepseek-ai/dsh-base': '0.1.0-rc.6',
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
writeInstalledPkg(profileDir, '@linxin666/dsh-ssh', '0.1.12', 'patch.yml');
|
|
92
|
+
writeInstalledPkg(profileDir, 'pkg-a', '1.0.0');
|
|
93
|
+
writeInstalledPkg(profileDir, 'dsh-memory-evolve', '1.0.0');
|
|
94
|
+
writeInstalledPkg(profileDir, '@deepseek-ai/dsh-base', '0.1.0-rc.6');
|
|
95
|
+
|
|
96
|
+
const list = listInstalledPlugins(homeDir, 'web');
|
|
97
|
+
const ssh = list.find((p) => p.name === '@linxin666/dsh-ssh');
|
|
98
|
+
const plain = list.find((p) => p.name === 'pkg-a');
|
|
99
|
+
const git = list.find((p) => p.name === 'dsh-memory-evolve');
|
|
100
|
+
assert.equal(ssh?.version, '0.1.12', '必须返回落盘真实版本,而不是声明的 ^0.1.0');
|
|
101
|
+
assert.equal(ssh?.isBundle, true);
|
|
102
|
+
assert.deepEqual(ssh?.inBundles, ['@linxin666/dsh-ssh'], '直接依赖的 bundle 自身就是 profile 层');
|
|
103
|
+
assert.equal(plain?.version, '1.0.0');
|
|
104
|
+
assert.equal(plain?.isBundle, false);
|
|
105
|
+
assert.deepEqual(plain?.inBundles, []);
|
|
106
|
+
// 声明 spec 原样保留:github 来源 / 版本区间都要随清单带出(导入重装依据)
|
|
107
|
+
assert.equal(git?.spec, 'github:csyangwen/dsh-memory-evolve');
|
|
108
|
+
assert.equal(ssh?.spec, '^0.1.0');
|
|
109
|
+
assert.equal(list.some((p) => p.name === '@deepseek-ai/dsh-base'), false, 'in-box bundle 不出现');
|
|
110
|
+
assert.equal(list.every((p: PluginInfo) => p.enabled === true), true, '文件视图依赖即视为启用');
|
|
111
|
+
} finally {
|
|
112
|
+
cleanup();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('listInstalledPlugins: 声明了依赖但未实际安装 → version 空串不抛', () => {
|
|
117
|
+
const { homeDir, profileDir, cleanup } = makeTempProfile({ 'ghost-dep': '^9.9.9' });
|
|
118
|
+
try {
|
|
119
|
+
// 不写 node_modules/ghost-dep:node_modules 缺失不应让 listInstalled 抛错
|
|
120
|
+
const list = listInstalledPlugins(homeDir, 'web');
|
|
121
|
+
assert.equal(list.length, 1);
|
|
122
|
+
assert.equal(list[0]?.name, 'ghost-dep');
|
|
123
|
+
assert.equal(list[0]?.version, '', '未落盘 → 版本空串(不抛)');
|
|
124
|
+
} finally {
|
|
125
|
+
cleanup();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('issue #28: 仅由 dsh.profile.bundles 声明的层必须出现在已装清单里(此前完全不可见)', () => {
|
|
130
|
+
// 场景:用户手动把社区 bundle 写进 dsh.profile.bundles(或某个安装路径只写了 bundles 而没写
|
|
131
|
+
// dependencies)。reconcileBundles 对这类条目是**保留**的(移除条件要求曾是依赖),
|
|
132
|
+
// 所以 DSH 启动时确实会挂载它 —— 但旧清单只遍历 dependencies,于是「装了却识别不到」。
|
|
133
|
+
const { homeDir, profileDir, cleanup } = makeTempProfile(
|
|
134
|
+
{ 'pkg-a': '1.0.0', '@deepseek-ai/dsh-base': '0.1.0-rc.6' },
|
|
135
|
+
['@linxin666/dsh-web-ui-all', 'stale-pkg', '@deepseek-ai/dsh-base'],
|
|
136
|
+
);
|
|
137
|
+
try {
|
|
138
|
+
writeInstalledPkg(profileDir, 'pkg-a', '1.0.0');
|
|
139
|
+
writeInstalledPkg(profileDir, '@linxin666/dsh-web-ui-all', '0.3.1', 'patch.yml');
|
|
140
|
+
writeInstalledPkg(profileDir, '@deepseek-ai/dsh-base', '0.1.0-rc.6');
|
|
141
|
+
|
|
142
|
+
const list = listInstalledPlugins(homeDir, 'web');
|
|
143
|
+
const bundleOnly = list.find((p) => p.name === '@linxin666/dsh-web-ui-all');
|
|
144
|
+
assert.ok(bundleOnly !== undefined, '仅 bundles 声明的层必须出现在清单里');
|
|
145
|
+
assert.equal(bundleOnly?.version, '0.3.1', '版本取 node_modules 落盘版本');
|
|
146
|
+
assert.equal(bundleOnly?.isBundle, true);
|
|
147
|
+
assert.deepEqual(bundleOnly?.inBundles, ['@linxin666/dsh-web-ui-all']);
|
|
148
|
+
assert.equal(bundleOnly?.spec, undefined, '无依赖声明 → 无 spec(导入按裸包名装最新版)');
|
|
149
|
+
// 从未落盘的 bundles 条目:仍列出(它确实是启动时会挂载的层),版本空串不抛
|
|
150
|
+
assert.equal(list.find((p) => p.name === 'stale-pkg')?.version, '');
|
|
151
|
+
// in-box bundle 依然不出现(无论来自 dependencies 还是 bundles)
|
|
152
|
+
assert.equal(list.some((p) => p.name === '@deepseek-ai/dsh-base'), false);
|
|
153
|
+
// 依赖里的普通插件不受影响
|
|
154
|
+
assert.equal(list.find((p) => p.name === 'pkg-a')?.spec, '1.0.0');
|
|
155
|
+
} finally {
|
|
156
|
+
cleanup();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('reconcileBundles: 当前依赖但非 bundle 的条目移出;从未是依赖的手动条目保留;无变化不写回', () => {
|
|
161
|
+
// 语义对齐官方 dsh reconcilePlugins(plugin-9h8shc4d.js):移除条件 =
|
|
162
|
+
// 「(之前或当前)是依赖 且 不再声明 bundle patch」——从未是依赖的 bundles 条目
|
|
163
|
+
// 视为手动维护的层,永远保留(in-box 同理不碰)。
|
|
164
|
+
const { homeDir, profileDir, cleanup } = makeTempProfile(
|
|
165
|
+
{ 'pkg-bundle': '1.0.0', 'pkg-plain': '1.0.0' },
|
|
166
|
+
['pkg-plain', 'stale-pkg'],
|
|
167
|
+
);
|
|
168
|
+
try {
|
|
169
|
+
writeInstalledPkg(profileDir, 'pkg-bundle', '1.0.0', 'patch.yml');
|
|
170
|
+
writeInstalledPkg(profileDir, 'pkg-plain', '1.0.0');
|
|
171
|
+
|
|
172
|
+
assert.equal(reconcileBundles(profileDir), true, '有变化 → 写回并返回 true');
|
|
173
|
+
const after = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
174
|
+
assert.deepEqual(
|
|
175
|
+
after.dsh.profile.bundles,
|
|
176
|
+
['stale-pkg', 'pkg-bundle'],
|
|
177
|
+
'pkg-plain 是当前依赖但非 bundle → 移出;stale-pkg 从未是依赖 → 保留;pkg-bundle 追加',
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
assert.equal(reconcileBundles(profileDir), false, '已一致 → 不写回返回 false');
|
|
181
|
+
} finally {
|
|
182
|
+
cleanup();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin-cli 失败分类器测试(M2):classifyDshPluginFailure / isTransientDshPluginFailure /
|
|
3
|
+
* installErrorFor。纯函数,样本文本取自 pnpm 真实诊断形态(dshmarket pnpm-compat 矩阵)。
|
|
4
|
+
*/
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import {
|
|
8
|
+
classifyDshPluginFailure, installErrorFor, isTransientDshPluginFailure,
|
|
9
|
+
resolveProfileNameFromArgv,
|
|
10
|
+
type DshPluginResult,
|
|
11
|
+
} from './plugin-cli.ts';
|
|
12
|
+
|
|
13
|
+
test('resolveProfileNameFromArgv: --profile 取值 / 缺省 web / 非法值宽容回退', () => {
|
|
14
|
+
assert.equal(resolveProfileNameFromArgv(['--profile', 'tui']), 'tui');
|
|
15
|
+
assert.equal(resolveProfileNameFromArgv(['web', '--profile', 'headless', 'x']), 'headless');
|
|
16
|
+
assert.equal(resolveProfileNameFromArgv(['--profile']), 'web', '缺值回退');
|
|
17
|
+
assert.equal(resolveProfileNameFromArgv(['--profile', '--flag']), 'web', '值以 - 开头回退');
|
|
18
|
+
assert.equal(resolveProfileNameFromArgv([]), 'web');
|
|
19
|
+
assert.equal(resolveProfileNameFromArgv(['--profile', '../evil']), 'web', '非法值不抛错回退(消息构建路径安全)');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('classify: ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF → hoist-pattern-diff (recoverable)', () => {
|
|
23
|
+
const f = classifyDshPluginFailure('ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF\nsome pnpm output');
|
|
24
|
+
assert.equal(f?.code, 'hoist-pattern-diff');
|
|
25
|
+
assert.equal(f?.recoverable, true);
|
|
26
|
+
assert.match(f!.message, /pnpm install/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('classify: ERR_PNPM_ADDING_TO_ROOT → adding-to-root', () => {
|
|
30
|
+
const f = classifyDshPluginFailure('ERR_PNPM_ADDING_TO_ROOT Running this command will add the dependency to the workspace root');
|
|
31
|
+
assert.equal(f?.code, 'adding-to-root');
|
|
32
|
+
assert.match(f!.message, /add -w/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('classify: --workspace-root may only be used inside a workspace → not-a-workspace', () => {
|
|
36
|
+
const f = classifyDshPluginFailure('ERR_PNPM_WORKSPACE_ROOT "--workspace-root may only be used inside a workspace"');
|
|
37
|
+
assert.equal(f?.code, 'not-a-workspace');
|
|
38
|
+
assert.equal(f?.recoverable, false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('classify: minimumReleaseAge(两个错误码)→ release-age-violation', () => {
|
|
42
|
+
const a = classifyDshPluginFailure('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION');
|
|
43
|
+
const b = classifyDshPluginFailure('ERR_PNPM_NO_MATURE_MATCHING_VERSION');
|
|
44
|
+
assert.equal(a?.code, 'release-age-violation');
|
|
45
|
+
assert.equal(b?.code, 'release-age-violation');
|
|
46
|
+
assert.match(a!.message, /minimumReleaseAge/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('classify: git 构建脚本 allowBuilds 拦截(pnpm 10 句子 / pnpm 11 错误码)→ git-build-blocked', () => {
|
|
50
|
+
const sentence = 'The git-hosted package "dsh-memory-evolve@0.1.0" needs to execute build scripts but is not in the "allowBuilds" allowlist.';
|
|
51
|
+
const a = classifyDshPluginFailure(sentence);
|
|
52
|
+
const b = classifyDshPluginFailure('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED');
|
|
53
|
+
assert.equal(a?.code, 'git-build-blocked');
|
|
54
|
+
assert.equal(b?.code, 'git-build-blocked');
|
|
55
|
+
assert.match(a!.message, /allowBuilds/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('classify: patchedDependencies 的 patch 文件缺失 → patch-file-missing(可操作)', () => {
|
|
59
|
+
const zhOut = 'Error: × adding a new package\n╰─▶ Failed to read patch file C:\\Users\\x\\.dsh\\profiles\\web\\patches/dsh-approval-gate.patch:\n 系统找不到指定的文件。 (os error 2)';
|
|
60
|
+
const f = classifyDshPluginFailure(zhOut);
|
|
61
|
+
assert.equal(f?.code, 'patch-file-missing');
|
|
62
|
+
assert.equal(f?.recoverable, false, '需要人工把 patch 文件放回或删掉声明,重试不会自愈');
|
|
63
|
+
assert.match(f!.message, /patches\/dsh-approval-gate\.patch/, '必须点名缺失的 patch 文件');
|
|
64
|
+
assert.match(f!.message, /patchedDependencies/);
|
|
65
|
+
// 英文形态同样识别
|
|
66
|
+
assert.equal(classifyDshPluginFailure('Failed to read patch file /home/u/.dsh/profiles/web/patches/a.patch: ENOENT')?.code, 'patch-file-missing');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('classify: ERR_PNPM_FETCH_404 → fetch-404(含包名提取)', () => {
|
|
70
|
+
const out = 'ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/some-ghost-pkg: Not Found - 404';
|
|
71
|
+
const f = classifyDshPluginFailure(out);
|
|
72
|
+
assert.equal(f?.code, 'fetch-404');
|
|
73
|
+
assert.match(f!.message, /some-ghost-pkg/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('classify: 瞬时网络(各形态)→ transient-network,recoverable', () => {
|
|
77
|
+
const samples = [
|
|
78
|
+
'ERR_PNPM_FETCH_503 Service Unavailable',
|
|
79
|
+
'ERR_PNPM_META_FETCH_FAIL GET https://registry.npmjs.org/x: request failed',
|
|
80
|
+
'FetchError: request to https://registry.npmjs.org/x failed, reason: socket hang up',
|
|
81
|
+
'fetch failed: ECONNRESET',
|
|
82
|
+
'ETIMEDOUT',
|
|
83
|
+
'EAI_AGAIN',
|
|
84
|
+
'ENETUNREACH',
|
|
85
|
+
'network timeout',
|
|
86
|
+
];
|
|
87
|
+
for (const s of samples) {
|
|
88
|
+
assert.equal(classifyDshPluginFailure(s)?.code, 'transient-network', `sample: ${s}`);
|
|
89
|
+
}
|
|
90
|
+
assert.equal(isTransientDshPluginFailure('ERR_PNPM_FETCH_503'), true);
|
|
91
|
+
assert.equal(isTransientDshPluginFailure('ECONNRESET'), true);
|
|
92
|
+
assert.equal(isTransientDshPluginFailure('ERR_PNPM_FETCH_404'), false, '404 不算瞬时网络');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('classify: pnpm not found → pnpm-missing', () => {
|
|
96
|
+
const f = classifyDshPluginFailure('dsh: pnpm not found on PATH — install pnpm to manage profile plugins');
|
|
97
|
+
assert.equal(f?.code, 'pnpm-missing');
|
|
98
|
+
assert.match(f!.message, /pnpm/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('classify: 未识别输出 → null', () => {
|
|
102
|
+
assert.equal(classifyDshPluginFailure('some random pnpm error text'), null);
|
|
103
|
+
assert.equal(classifyDshPluginFailure(''), null);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('installErrorFor: spawnError(ENOENT) → dsh 不可用提示', () => {
|
|
107
|
+
const r: DshPluginResult = { exitCode: 127, timedOut: false, stdout: '', stderr: '', spawnError: 'spawn dsh ENOENT' };
|
|
108
|
+
const e = installErrorFor('pkg-a', r);
|
|
109
|
+
assert.match(e.message, /dsh CLI/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('installErrorFor: timedOut → 超时提示', () => {
|
|
113
|
+
const r: DshPluginResult = { exitCode: null, timedOut: true, stdout: '', stderr: '' };
|
|
114
|
+
const e = installErrorFor('pkg-a', r);
|
|
115
|
+
assert.match(e.message, /超时/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('installErrorFor: 已分类失败 → 双语可读消息 + code', () => {
|
|
119
|
+
const r: DshPluginResult = {
|
|
120
|
+
exitCode: 1, timedOut: false,
|
|
121
|
+
stdout: '', stderr: 'ERR_PNPM_FETCH_503 Service Unavailable',
|
|
122
|
+
};
|
|
123
|
+
const e = installErrorFor('pkg-a', r);
|
|
124
|
+
assert.match(e.message, /transient-network/);
|
|
125
|
+
assert.match(e.message, /网络临时失败/);
|
|
126
|
+
assert.match(e.message, /transient network/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('installErrorFor: 未识别 → stderr 尾部摘要', () => {
|
|
130
|
+
const r: DshPluginResult = {
|
|
131
|
+
exitCode: 1, timedOut: false,
|
|
132
|
+
stdout: 'line1', stderr: 'mystery error line\nanother line',
|
|
133
|
+
};
|
|
134
|
+
const e = installErrorFor('pkg-a', r);
|
|
135
|
+
assert.match(e.message, /exit 1/);
|
|
136
|
+
assert.match(e.message, /mystery error line/);
|
|
137
|
+
});
|