@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.
Files changed (429) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/cordis.patch.yml +21 -0
  4. package/lib/adapters/agent-instructions.d.ts +13 -0
  5. package/lib/adapters/agent-instructions.js +46 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/credentials.d.ts +37 -0
  9. package/lib/adapters/credentials.js +122 -0
  10. package/lib/adapters/file-collection.d.ts +15 -0
  11. package/lib/adapters/file-collection.js +127 -0
  12. package/lib/adapters/index.d.ts +57 -0
  13. package/lib/adapters/index.js +55 -0
  14. package/lib/adapters/link-report.d.ts +16 -0
  15. package/lib/adapters/link-report.js +47 -0
  16. package/lib/adapters/mcp.d.ts +28 -0
  17. package/lib/adapters/mcp.js +178 -0
  18. package/lib/adapters/plugin-files.d.ts +18 -0
  19. package/lib/adapters/plugin-files.js +142 -0
  20. package/lib/adapters/plugins.d.ts +41 -0
  21. package/lib/adapters/plugins.js +603 -0
  22. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  23. package/lib/adapters/pnpm-workspace.js +147 -0
  24. package/lib/adapters/prompts.d.ts +30 -0
  25. package/lib/adapters/prompts.js +231 -0
  26. package/lib/adapters/providers.d.ts +34 -0
  27. package/lib/adapters/providers.js +155 -0
  28. package/lib/adapters/self.d.ts +17 -0
  29. package/lib/adapters/self.js +71 -0
  30. package/lib/adapters/sessions.d.ts +14 -0
  31. package/lib/adapters/sessions.js +15 -0
  32. package/lib/adapters/settings.d.ts +28 -0
  33. package/lib/adapters/settings.js +165 -0
  34. package/lib/adapters/skills.d.ts +13 -0
  35. package/lib/adapters/skills.js +14 -0
  36. package/lib/adapters/test-helpers.d.ts +125 -0
  37. package/lib/adapters/test-helpers.js +236 -0
  38. package/lib/adapters/ui.d.ts +27 -0
  39. package/lib/adapters/ui.js +89 -0
  40. package/lib/adapters/workspaces.d.ts +13 -0
  41. package/lib/adapters/workspaces.js +106 -0
  42. package/lib/client.d.ts +1026 -0
  43. package/lib/client.js +10047 -0
  44. package/lib/core/analyzer.d.ts +99 -0
  45. package/lib/core/analyzer.js +934 -0
  46. package/lib/core/backup.d.ts +138 -0
  47. package/lib/core/backup.js +513 -0
  48. package/lib/core/boot-rescue.d.ts +146 -0
  49. package/lib/core/boot-rescue.js +618 -0
  50. package/lib/core/cache-cleaner.d.ts +31 -0
  51. package/lib/core/cache-cleaner.js +95 -0
  52. package/lib/core/config-lifecycle.d.ts +180 -0
  53. package/lib/core/config-lifecycle.js +481 -0
  54. package/lib/core/config-snapshot.d.ts +135 -0
  55. package/lib/core/config-snapshot.js +424 -0
  56. package/lib/core/config-state.d.ts +70 -0
  57. package/lib/core/config-state.js +177 -0
  58. package/lib/core/consult-source.d.ts +58 -0
  59. package/lib/core/consult-source.js +260 -0
  60. package/lib/core/crash-report.d.ts +84 -0
  61. package/lib/core/crash-report.js +317 -0
  62. package/lib/core/exporter.d.ts +68 -0
  63. package/lib/core/exporter.js +375 -0
  64. package/lib/core/ghost-sweep.d.ts +19 -0
  65. package/lib/core/ghost-sweep.js +52 -0
  66. package/lib/core/importer.d.ts +60 -0
  67. package/lib/core/importer.js +29 -0
  68. package/lib/core/index.d.ts +14 -0
  69. package/lib/core/index.js +14 -0
  70. package/lib/core/journal.d.ts +197 -0
  71. package/lib/core/journal.js +418 -0
  72. package/lib/core/local-plugin-host.d.ts +35 -0
  73. package/lib/core/local-plugin-host.js +120 -0
  74. package/lib/core/local-plugin-pack.d.ts +135 -0
  75. package/lib/core/local-plugin-pack.js +278 -0
  76. package/lib/core/messages.d.ts +310 -0
  77. package/lib/core/messages.js +621 -0
  78. package/lib/core/migration-consult.d.ts +117 -0
  79. package/lib/core/migration-consult.js +363 -0
  80. package/lib/core/migration-history.d.ts +153 -0
  81. package/lib/core/migration-history.js +411 -0
  82. package/lib/core/model-tools.d.ts +50 -0
  83. package/lib/core/model-tools.js +148 -0
  84. package/lib/core/msg-types.d.ts +8 -0
  85. package/lib/core/msg-types.js +6 -0
  86. package/lib/core/phase3-child-crash.d.ts +1 -0
  87. package/lib/core/phase3-child-crash.js +87 -0
  88. package/lib/core/phase3-host.d.ts +138 -0
  89. package/lib/core/phase3-host.js +359 -0
  90. package/lib/core/phase3-prod-child.d.ts +1 -0
  91. package/lib/core/phase3-prod-child.js +35 -0
  92. package/lib/core/phase4-crash-child.d.ts +1 -0
  93. package/lib/core/phase4-crash-child.js +103 -0
  94. package/lib/core/plugin-cli.d.ts +150 -0
  95. package/lib/core/plugin-cli.js +497 -0
  96. package/lib/core/reconcile.d.ts +120 -0
  97. package/lib/core/reconcile.js +389 -0
  98. package/lib/core/recovery-orchestrator.d.ts +94 -0
  99. package/lib/core/recovery-orchestrator.js +327 -0
  100. package/lib/core/restore.d.ts +162 -0
  101. package/lib/core/restore.js +703 -0
  102. package/lib/core/rollback.d.ts +12 -0
  103. package/lib/core/rollback.js +145 -0
  104. package/lib/core/run-registry.d.ts +85 -0
  105. package/lib/core/run-registry.js +157 -0
  106. package/lib/core/startup-barrier.d.ts +53 -0
  107. package/lib/core/startup-barrier.js +77 -0
  108. package/lib/core/types.d.ts +515 -0
  109. package/lib/core/types.js +27 -0
  110. package/lib/core/undo.d.ts +73 -0
  111. package/lib/core/undo.js +87 -0
  112. package/lib/core/validator.d.ts +18 -0
  113. package/lib/core/validator.js +77 -0
  114. package/lib/core/verify-recovery.d.ts +21 -0
  115. package/lib/core/verify-recovery.js +256 -0
  116. package/lib/core/watcher.d.ts +101 -0
  117. package/lib/core/watcher.js +219 -0
  118. package/lib/index.d.ts +238 -0
  119. package/lib/index.js +2764 -0
  120. package/lib/migrations/index.d.ts +29 -0
  121. package/lib/migrations/index.js +56 -0
  122. package/lib/migrations/v1-to-v2.d.ts +9 -0
  123. package/lib/migrations/v1-to-v2.js +10 -0
  124. package/lib/profiles/index.d.ts +4 -0
  125. package/lib/profiles/index.js +5 -0
  126. package/lib/profiles/profile-manager.d.ts +106 -0
  127. package/lib/profiles/profile-manager.js +543 -0
  128. package/lib/schema/config.d.ts +28 -0
  129. package/lib/schema/config.js +165 -0
  130. package/lib/schema/index.d.ts +24 -0
  131. package/lib/schema/index.js +26 -0
  132. package/lib/schema/manifest.d.ts +28 -0
  133. package/lib/schema/manifest.js +109 -0
  134. package/lib/schema/tombstones.d.ts +46 -0
  135. package/lib/schema/tombstones.js +64 -0
  136. package/lib/schema/types.d.ts +232 -0
  137. package/lib/schema/types.js +8 -0
  138. package/lib/schema/versions.d.ts +27 -0
  139. package/lib/schema/versions.js +50 -0
  140. package/lib/security/encryption.d.ts +85 -0
  141. package/lib/security/encryption.js +279 -0
  142. package/lib/security/index.d.ts +16 -0
  143. package/lib/security/index.js +17 -0
  144. package/lib/security/integrity.d.ts +30 -0
  145. package/lib/security/integrity.js +78 -0
  146. package/lib/security/redaction.d.ts +26 -0
  147. package/lib/security/redaction.js +95 -0
  148. package/lib/security/secret-scanner.d.ts +86 -0
  149. package/lib/security/secret-scanner.js +404 -0
  150. package/lib/security/vault.d.ts +59 -0
  151. package/lib/security/vault.js +151 -0
  152. package/lib/security/zip-security.d.ts +27 -0
  153. package/lib/security/zip-security.js +154 -0
  154. package/lib/sync/autosync-config.d.ts +39 -0
  155. package/lib/sync/autosync-config.js +151 -0
  156. package/lib/sync/autosync-scheduler.d.ts +139 -0
  157. package/lib/sync/autosync-scheduler.js +564 -0
  158. package/lib/sync/fs.d.ts +18 -0
  159. package/lib/sync/fs.js +49 -0
  160. package/lib/sync/git/git-transport.d.ts +100 -0
  161. package/lib/sync/git/git-transport.js +437 -0
  162. package/lib/sync/github-auth.d.ts +131 -0
  163. package/lib/sync/github-auth.js +264 -0
  164. package/lib/sync/layout.d.ts +43 -0
  165. package/lib/sync/layout.js +203 -0
  166. package/lib/sync/snapshot-json.d.ts +9 -0
  167. package/lib/sync/snapshot-json.js +107 -0
  168. package/lib/sync/sync-config.d.ts +81 -0
  169. package/lib/sync/sync-config.js +281 -0
  170. package/lib/sync/sync-engine.d.ts +269 -0
  171. package/lib/sync/sync-engine.js +591 -0
  172. package/lib/sync/sync-history.d.ts +42 -0
  173. package/lib/sync/sync-history.js +78 -0
  174. package/lib/sync/sync-selection.d.ts +29 -0
  175. package/lib/sync/sync-selection.js +127 -0
  176. package/lib/sync/sync-session.d.ts +40 -0
  177. package/lib/sync/sync-session.js +54 -0
  178. package/lib/sync/sync-state.d.ts +37 -0
  179. package/lib/sync/sync-state.js +108 -0
  180. package/lib/sync/transport.d.ts +75 -0
  181. package/lib/sync/transport.js +55 -0
  182. package/lib/sync/ui-prefs.d.ts +34 -0
  183. package/lib/sync/ui-prefs.js +109 -0
  184. package/lib/sync/webdav/webdav-transport.d.ts +77 -0
  185. package/lib/sync/webdav/webdav-transport.js +451 -0
  186. package/lib/ui/errors.d.ts +19 -0
  187. package/lib/ui/errors.js +81 -0
  188. package/lib/ui/export-flow.d.ts +82 -0
  189. package/lib/ui/export-flow.js +98 -0
  190. package/lib/ui/i18n.d.ts +289 -0
  191. package/lib/ui/i18n.js +581 -0
  192. package/lib/ui/migration-consult-view.d.ts +91 -0
  193. package/lib/ui/migration-consult-view.js +115 -0
  194. package/lib/ui/progress.d.ts +38 -0
  195. package/lib/ui/progress.js +87 -0
  196. package/lib/ui/report.d.ts +23 -0
  197. package/lib/ui/report.js +153 -0
  198. package/lib/ui/test-helpers.d.ts +69 -0
  199. package/lib/ui/test-helpers.js +154 -0
  200. package/lib/ui/types.d.ts +257 -0
  201. package/lib/ui/types.js +21 -0
  202. package/lib/utils/atomic-write.d.ts +88 -0
  203. package/lib/utils/atomic-write.js +399 -0
  204. package/lib/utils/env-lock.d.ts +393 -0
  205. package/lib/utils/env-lock.js +930 -0
  206. package/lib/utils/hashing.d.ts +17 -0
  207. package/lib/utils/hashing.js +41 -0
  208. package/lib/utils/json.d.ts +24 -0
  209. package/lib/utils/json.js +76 -0
  210. package/lib/utils/logger.d.ts +39 -0
  211. package/lib/utils/logger.js +82 -0
  212. package/lib/utils/paths.d.ts +42 -0
  213. package/lib/utils/paths.js +189 -0
  214. package/lib/utils/proxy.d.ts +69 -0
  215. package/lib/utils/proxy.js +455 -0
  216. package/lib/utils/recursive-walk.d.ts +24 -0
  217. package/lib/utils/recursive-walk.js +134 -0
  218. package/lib/utils/zip.d.ts +54 -0
  219. package/lib/utils/zip.js +285 -0
  220. package/package.json +188 -0
  221. package/src/adapters/agent-instructions.ts +48 -0
  222. package/src/adapters/agent-presets.ts +14 -0
  223. package/src/adapters/credentials.test.ts +90 -0
  224. package/src/adapters/credentials.ts +143 -0
  225. package/src/adapters/file-collection.ts +137 -0
  226. package/src/adapters/files.test.ts +353 -0
  227. package/src/adapters/index.ts +94 -0
  228. package/src/adapters/link-report.test.ts +65 -0
  229. package/src/adapters/link-report.ts +61 -0
  230. package/src/adapters/mcp.test.ts +96 -0
  231. package/src/adapters/mcp.ts +185 -0
  232. package/src/adapters/plugin-files.ts +146 -0
  233. package/src/adapters/plugins.test.ts +326 -0
  234. package/src/adapters/plugins.ts +615 -0
  235. package/src/adapters/pnpm-workspace.test.ts +87 -0
  236. package/src/adapters/pnpm-workspace.ts +150 -0
  237. package/src/adapters/prompts.test.ts +110 -0
  238. package/src/adapters/prompts.ts +230 -0
  239. package/src/adapters/providers.test.ts +126 -0
  240. package/src/adapters/providers.ts +173 -0
  241. package/src/adapters/roundtrip.test.ts +190 -0
  242. package/src/adapters/self.test.ts +92 -0
  243. package/src/adapters/self.ts +75 -0
  244. package/src/adapters/sessions.ts +15 -0
  245. package/src/adapters/settings.test.ts +100 -0
  246. package/src/adapters/settings.ts +187 -0
  247. package/src/adapters/skills.ts +14 -0
  248. package/src/adapters/test-helpers.ts +248 -0
  249. package/src/adapters/ui.test.ts +57 -0
  250. package/src/adapters/ui.ts +108 -0
  251. package/src/adapters/workspaces.test.ts +106 -0
  252. package/src/adapters/workspaces.ts +109 -0
  253. package/src/client/ConfigManagerSection.tsx +90 -0
  254. package/src/client/api.ts +66 -0
  255. package/src/client/client-types.ts +18 -0
  256. package/src/client/common/ErrorBanner.tsx +73 -0
  257. package/src/client/common/Icon.tsx +129 -0
  258. package/src/client/common/Modal.tsx +173 -0
  259. package/src/client/common/ToastViewport.tsx +73 -0
  260. package/src/client/common/toast-store.test.ts +192 -0
  261. package/src/client/common/toast-store.ts +181 -0
  262. package/src/client/common/toast-types.ts +7 -0
  263. package/src/client/common/ui.tsx +342 -0
  264. package/src/client/config-manager.module.css +2971 -0
  265. package/src/client/css-modules.d.ts +8 -0
  266. package/src/client/index.ts +65 -0
  267. package/src/client/locales.ts +56 -0
  268. package/src/client/lucide-icons.d.ts +38 -0
  269. package/src/client/run-store.test.ts +223 -0
  270. package/src/client/run-store.ts +372 -0
  271. package/src/client/sync/SyncConfirmView.tsx +379 -0
  272. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  273. package/src/client/sync/SyncHistoryView.tsx +220 -0
  274. package/src/client/sync/SyncSettingsView.tsx +1535 -0
  275. package/src/client/sync/history-model.test.ts +194 -0
  276. package/src/client/sync/history-model.ts +195 -0
  277. package/src/client/sync/sync-api.test.ts +585 -0
  278. package/src/client/sync/sync-api.ts +543 -0
  279. package/src/client/sync/sync-locales.ts +416 -0
  280. package/src/client/sync/sync-push-preview.test.ts +52 -0
  281. package/src/client/sync/sync-view-v2.test.ts +213 -0
  282. package/src/client/sync/sync-view.test.ts +487 -0
  283. package/src/client/sync/sync-view.ts +759 -0
  284. package/src/core/analyzer.ts +1068 -0
  285. package/src/core/backup.ts +605 -0
  286. package/src/core/boot-rescue.test.ts +847 -0
  287. package/src/core/boot-rescue.ts +737 -0
  288. package/src/core/cache-cleaner.test.ts +169 -0
  289. package/src/core/cache-cleaner.ts +121 -0
  290. package/src/core/config-lifecycle.test.ts +727 -0
  291. package/src/core/config-lifecycle.ts +573 -0
  292. package/src/core/config-snapshot.test.ts +369 -0
  293. package/src/core/config-snapshot.ts +522 -0
  294. package/src/core/config-state.test.ts +381 -0
  295. package/src/core/config-state.ts +207 -0
  296. package/src/core/consult-source.test.ts +199 -0
  297. package/src/core/consult-source.ts +293 -0
  298. package/src/core/crash-report.test.ts +380 -0
  299. package/src/core/crash-report.ts +369 -0
  300. package/src/core/exporter.ts +417 -0
  301. package/src/core/ghost-sweep.test.ts +94 -0
  302. package/src/core/ghost-sweep.ts +67 -0
  303. package/src/core/importer.ts +83 -0
  304. package/src/core/index.ts +56 -0
  305. package/src/core/journal.test.ts +257 -0
  306. package/src/core/journal.ts +518 -0
  307. package/src/core/local-plugin-host.ts +152 -0
  308. package/src/core/local-plugin-pack.test.ts +359 -0
  309. package/src/core/local-plugin-pack.ts +363 -0
  310. package/src/core/messages.test.ts +54 -0
  311. package/src/core/messages.ts +664 -0
  312. package/src/core/migration-consult.test.ts +315 -0
  313. package/src/core/migration-consult.ts +494 -0
  314. package/src/core/migration-history.test.ts +319 -0
  315. package/src/core/migration-history.ts +484 -0
  316. package/src/core/model-tools.test.ts +190 -0
  317. package/src/core/model-tools.ts +208 -0
  318. package/src/core/msg-types.ts +10 -0
  319. package/src/core/phase3-child-crash.ts +95 -0
  320. package/src/core/phase3-host.ts +411 -0
  321. package/src/core/phase3-p1.test.ts +212 -0
  322. package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
  323. package/src/core/phase3-prod-child.ts +36 -0
  324. package/src/core/phase3-production-integration.test.ts +286 -0
  325. package/src/core/phase4-crash-child.ts +101 -0
  326. package/src/core/phase4-crash-injection.test.ts +166 -0
  327. package/src/core/plugin-cli.fs.test.ts +184 -0
  328. package/src/core/plugin-cli.test.ts +137 -0
  329. package/src/core/plugin-cli.ts +563 -0
  330. package/src/core/reconcile.test.ts +392 -0
  331. package/src/core/reconcile.ts +498 -0
  332. package/src/core/recovery-orchestrator.test.ts +145 -0
  333. package/src/core/recovery-orchestrator.ts +366 -0
  334. package/src/core/restore-manage.test.ts +94 -0
  335. package/src/core/restore.ts +849 -0
  336. package/src/core/rollback.ts +151 -0
  337. package/src/core/run-progress.test.ts +237 -0
  338. package/src/core/run-registry.test.ts +232 -0
  339. package/src/core/run-registry.ts +209 -0
  340. package/src/core/smoke.test.ts +842 -0
  341. package/src/core/startup-barrier.ts +99 -0
  342. package/src/core/types.ts +542 -0
  343. package/src/core/undo.ts +116 -0
  344. package/src/core/validator.ts +84 -0
  345. package/src/core/verify-recovery.ts +265 -0
  346. package/src/core/watcher.test.ts +326 -0
  347. package/src/core/watcher.ts +263 -0
  348. package/src/index.facade.test.ts +265 -0
  349. package/src/index.sync.test.ts +217 -0
  350. package/src/index.ts +2975 -0
  351. package/src/migrations/index.ts +71 -0
  352. package/src/migrations/v1-to-v2.ts +17 -0
  353. package/src/profiles/index.ts +8 -0
  354. package/src/profiles/profile-manager.test.ts +282 -0
  355. package/src/profiles/profile-manager.ts +630 -0
  356. package/src/schema/config.ts +165 -0
  357. package/src/schema/index.ts +71 -0
  358. package/src/schema/manifest-schema.test.ts +519 -0
  359. package/src/schema/manifest.ts +124 -0
  360. package/src/schema/tombstones.test.ts +131 -0
  361. package/src/schema/tombstones.ts +89 -0
  362. package/src/schema/types.ts +245 -0
  363. package/src/schema/versions.ts +55 -0
  364. package/src/security/encryption.ts +335 -0
  365. package/src/security/index.ts +16 -0
  366. package/src/security/integrity.ts +102 -0
  367. package/src/security/redaction.ts +101 -0
  368. package/src/security/secret-scanner.ts +445 -0
  369. package/src/security/security.test.ts +1215 -0
  370. package/src/security/vault.test.ts +164 -0
  371. package/src/security/vault.ts +198 -0
  372. package/src/security/zip-security.ts +180 -0
  373. package/src/sync/autosync-config.test.ts +147 -0
  374. package/src/sync/autosync-config.ts +179 -0
  375. package/src/sync/autosync-scheduler.test.ts +490 -0
  376. package/src/sync/autosync-scheduler.ts +646 -0
  377. package/src/sync/fs.ts +48 -0
  378. package/src/sync/git/git-transport.test.ts +673 -0
  379. package/src/sync/git/git-transport.ts +491 -0
  380. package/src/sync/github-auth.test.ts +280 -0
  381. package/src/sync/github-auth.ts +356 -0
  382. package/src/sync/layout.test.ts +270 -0
  383. package/src/sync/layout.ts +222 -0
  384. package/src/sync/snapshot-json.test.ts +119 -0
  385. package/src/sync/snapshot-json.ts +128 -0
  386. package/src/sync/sync-config.test.ts +310 -0
  387. package/src/sync/sync-config.ts +299 -0
  388. package/src/sync/sync-engine.test.ts +1074 -0
  389. package/src/sync/sync-engine.ts +754 -0
  390. package/src/sync/sync-history.test.ts +116 -0
  391. package/src/sync/sync-history.ts +120 -0
  392. package/src/sync/sync-selection.test.ts +186 -0
  393. package/src/sync/sync-selection.ts +143 -0
  394. package/src/sync/sync-session.test.ts +143 -0
  395. package/src/sync/sync-session.ts +77 -0
  396. package/src/sync/sync-state.test.ts +164 -0
  397. package/src/sync/sync-state.ts +131 -0
  398. package/src/sync/transport.test.ts +111 -0
  399. package/src/sync/transport.ts +118 -0
  400. package/src/sync/ui-prefs.test.ts +152 -0
  401. package/src/sync/ui-prefs.ts +135 -0
  402. package/src/sync/webdav/webdav-transport.test.ts +871 -0
  403. package/src/sync/webdav/webdav-transport.ts +535 -0
  404. package/src/ui/errors.test.ts +47 -0
  405. package/src/ui/errors.ts +100 -0
  406. package/src/ui/export-flow.test.ts +87 -0
  407. package/src/ui/export-flow.ts +156 -0
  408. package/src/ui/i18n.ts +590 -0
  409. package/src/ui/migration-consult-view.test.ts +151 -0
  410. package/src/ui/migration-consult-view.ts +178 -0
  411. package/src/ui/progress.test.ts +47 -0
  412. package/src/ui/progress.ts +102 -0
  413. package/src/ui/report.test.ts +101 -0
  414. package/src/ui/report.ts +146 -0
  415. package/src/ui/test-helpers.ts +184 -0
  416. package/src/ui/types.ts +325 -0
  417. package/src/utils/atomic-write.test.ts +312 -0
  418. package/src/utils/atomic-write.ts +422 -0
  419. package/src/utils/env-lock.test.ts +999 -0
  420. package/src/utils/env-lock.ts +1086 -0
  421. package/src/utils/hashing.ts +53 -0
  422. package/src/utils/json.ts +77 -0
  423. package/src/utils/logger.ts +114 -0
  424. package/src/utils/paths.ts +176 -0
  425. package/src/utils/proxy.test.ts +465 -0
  426. package/src/utils/proxy.ts +493 -0
  427. package/src/utils/recursive-walk.test.ts +148 -0
  428. package/src/utils/recursive-walk.ts +145 -0
  429. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,487 @@
1
+ /**
2
+ * m-sync-ui:远程同步区块渲染模型单测(纯函数,node 可测,无需 DOM)。
3
+ * 覆盖验收:报告渲染(push/pull)、按钮状态、私有仓库提示、状态行。
4
+ */
5
+ import test from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+
8
+ import type { PullChange, SyncPullReport, SyncPushReport } from '../../sync/sync-engine.ts'
9
+ import type { SectionId } from '../../schema/types.ts'
10
+ import type { GithubPollResponse, SyncSectionInfo, SyncStatusResponse } from './sync-api.ts'
11
+ import {
12
+ autosyncIntervalMs, channelTabModels, computeAutosyncCountdown, computeGithubLoginView, computeRemoteReady, computeSyncButtons, computeSyncStatus,
13
+ defaultChannelSyncState, formatDateTime, formatIntervalDuration, formatLastSync, githubPollMessage, kindLabel, privateRepoHint,
14
+ pullReportView, pushReportView, presetById, presetIdForUrl, readStoredChannel, recommendedSyncSections,
15
+ severityLabel, summarizePullChanges, syncSectionGroups, syncSectionOptions, WEBDAV_PRESETS, writeStoredChannel,
16
+ } from './sync-view.ts'
17
+
18
+ /* ---------------------------------------------------------------- 私有仓库提示 */
19
+
20
+ test('sync-view: 私有仓库强制提示文案存在且强调私有', () => {
21
+ const hint = privateRepoHint()
22
+ assert.match(hint, /私有/)
23
+ assert.match(hint, /public/)
24
+ assert.match(hint, /token/)
25
+ })
26
+
27
+ /* ---------------------------------------------------------------- 同步分区模式(默认/高级) */
28
+
29
+ const SYNC_CATALOG: SyncSectionInfo[] = [
30
+ { id: 'settings', displayName: 'Settings', defaultIncluded: true },
31
+ { id: 'providers', displayName: 'Providers & Models', defaultIncluded: true },
32
+ { id: 'plugins', displayName: 'Plugins', defaultIncluded: true },
33
+ { id: 'skills', displayName: 'Skills', defaultIncluded: true },
34
+ ];
35
+
36
+ test('sync-view: syncSectionOptions 把 host 目录投影为勾选项(保 id 顺序 + 导出目录分组/描述补充)', () => {
37
+ const opts = syncSectionOptions(SYNC_CATALOG)
38
+ assert.deepEqual(opts.map((o) => o.id), ['settings', 'providers', 'plugins', 'skills'])
39
+ assert.equal(opts[0]?.label, 'Settings')
40
+ assert.equal(opts[0]?.defaultIncluded, true)
41
+ // 分组/描述来自导出目录(单一事实源):settings → general 组带描述;providers → ai 组
42
+ assert.equal(opts[0]?.group, 'general')
43
+ assert.match(opts[0]?.description ?? '', /DSH 全局设置/)
44
+ assert.equal(opts[1]?.group, 'ai')
45
+ assert.match(opts[1]?.description ?? '', /Provider/)
46
+ assert.equal(opts[2]?.group, 'extensions')
47
+ assert.equal(opts[3]?.group, 'customization')
48
+ });
49
+
50
+ test('sync-view: syncSectionOptions 未知分区 id → 兜底(description 空、group=general)', () => {
51
+ const opts = syncSectionOptions([
52
+ { id: 'nope' as SectionId, displayName: 'Nope', defaultIncluded: true },
53
+ ])
54
+ assert.equal(opts[0]?.label, 'Nope')
55
+ assert.equal(opts[0]?.description, '')
56
+ assert.equal(opts[0]?.group, 'general')
57
+ });
58
+
59
+ test('sync-view: syncSectionGroups 按导出分组投影(与「导出备份·自定义模式」同构,空组省略)', () => {
60
+ const groups = syncSectionGroups(syncSectionOptions(SYNC_CATALOG))
61
+ // settings(general) + providers(ai) + plugins(extensions) + skills(customization)
62
+ assert.deepEqual(groups.map((g) => g.group), ['general', 'ai', 'extensions', 'customization'])
63
+ assert.equal(groups[0]?.label, 'General')
64
+ assert.deepEqual(groups[0]?.items.map((i) => i.id), ['settings'])
65
+ assert.deepEqual(groups[2]?.items.map((i) => i.id), ['plugins'])
66
+ });
67
+
68
+ test('sync-view: recommendedSyncSections 只取默认包含的分区(默认模式)', () => {
69
+ const rec = recommendedSyncSections(SYNC_CATALOG)
70
+ assert.deepEqual(rec, ['settings', 'providers', 'plugins', 'skills'])
71
+ });
72
+
73
+ test('sync-view: recommendedSyncSections 排除非默认包含分区', () => {
74
+ const catalog: SyncSectionInfo[] = [
75
+ { id: 'settings', displayName: 'Settings', defaultIncluded: true },
76
+ { id: 'credentialsStatus', displayName: 'Credentials', defaultIncluded: true },
77
+ { id: 'mcp', displayName: 'MCP', defaultIncluded: true },
78
+ { id: 'pluginFiles', displayName: 'Plugin Files', defaultIncluded: false },
79
+ ];
80
+ assert.deepEqual(recommendedSyncSections(catalog), ['settings', 'credentialsStatus', 'mcp'])
81
+ });
82
+
83
+ test('sync-view: 空/缺省目录 → 推荐分区为空数组(UI 显示「至少选一个」场景)', () => {
84
+ assert.deepEqual(recommendedSyncSections([]), [])
85
+ assert.deepEqual(syncSectionOptions([]), [])
86
+ });
87
+
88
+ /* ---------------------------------------------------------------- 按钮状态 */
89
+
90
+ test('sync-view: 空闲 + 活动通道地址未就绪 → 两个按钮都禁用', () => {
91
+ const b = computeSyncButtons(null, false)
92
+ assert.equal(b.canPush, false)
93
+ assert.equal(b.canPull, false)
94
+ assert.equal(b.pushLabel, '推送到远端')
95
+ assert.equal(b.pullLabel, '拉取差异预览')
96
+ })
97
+
98
+ test('sync-view: 空闲 + 活动通道地址就绪 → 两个按钮可用', () => {
99
+ const b = computeSyncButtons(null, true)
100
+ assert.equal(b.canPush, true)
101
+ assert.equal(b.canPull, true)
102
+ })
103
+
104
+ test('sync-view: push 进行中 → 按钮禁用且文案切换为正在推送(防并发)', () => {
105
+ const b = computeSyncButtons('push', true)
106
+ assert.equal(b.canPush, false)
107
+ assert.equal(b.canPull, false)
108
+ assert.equal(b.pushLabel, '正在推送…')
109
+ assert.equal(b.pullLabel, '拉取差异预览')
110
+ })
111
+
112
+ test('sync-view: pull 进行中 → 两个按钮都禁用,pull 文案切换', () => {
113
+ const b = computeSyncButtons('pull', true)
114
+ assert.equal(b.canPush, false)
115
+ assert.equal(b.canPull, false)
116
+ assert.equal(b.pullLabel, '正在拉取…')
117
+ })
118
+
119
+ test('sync-view: computeRemoteReady 按活动通道判断地址就绪(git=repoUrl,webdav=url)', () => {
120
+ assert.equal(computeRemoteReady('git', 'https://github.com/u/r.git', ''), true)
121
+ assert.equal(computeRemoteReady('git', ' ', 'https://dav.example.com/dav'), false, 'git 通道不看 webdav 地址')
122
+ assert.equal(computeRemoteReady('webdav', '', 'https://dav.example.com/dav'), true)
123
+ assert.equal(computeRemoteReady('webdav', 'https://github.com/u/r.git', ''), false, 'webdav 通道不看 git 地址')
124
+ assert.equal(computeRemoteReady('webdav', '', ' '), false)
125
+ })
126
+
127
+ /* ---------------------------------------------------------------- 变更摘要 */
128
+
129
+ function change(overrides: Partial<PullChange>): PullChange {
130
+ return { id: 'x', adapter: 'settings', kind: 'Update', description: 'd', severity: 'info', ...overrides }
131
+ }
132
+
133
+ test('sync-view: summarizePullChanges 按 severity 计数', () => {
134
+ const summary = summarizePullChanges([
135
+ change({ severity: 'info' }),
136
+ change({ severity: 'info' }),
137
+ change({ severity: 'warning' }),
138
+ change({ severity: 'error' }),
139
+ ])
140
+ assert.equal(summary.total, 4)
141
+ assert.equal(summary.info, 2)
142
+ assert.equal(summary.warning, 1)
143
+ assert.equal(summary.error, 1)
144
+ assert.equal(summary.needsReview, false)
145
+ })
146
+
147
+ test('sync-view: summarizePullChanges 对冲突/密钥/依赖项标记 needsReview(安装插件不算)', () => {
148
+ // 插件安装随同步自动采用(product requirement),不标记 needsReview
149
+ const summary = summarizePullChanges([
150
+ change({ kind: 'Conflict' }),
151
+ change({ kind: 'Install' }),
152
+ change({ kind: 'Install' }),
153
+ ])
154
+ assert.equal(summary.needsReview, true)
155
+ // 仅 Install 项 → 不作为需人工决策项
156
+ const onlyInstall = summarizePullChanges([change({ kind: 'Install' })])
157
+ assert.equal(onlyInstall.needsReview, false)
158
+ })
159
+
160
+ test('sync-view: summarizePullChanges 空数组 → total 0 且不需决策', () => {
161
+ const summary = summarizePullChanges([])
162
+ assert.equal(summary.total, 0)
163
+ assert.equal(summary.needsReview, false)
164
+ })
165
+
166
+ test('sync-view: kindLabel / severityLabel 覆盖关键类型', () => {
167
+ assert.equal(kindLabel('Conflict'), '冲突')
168
+ assert.equal(kindLabel('Install'), '安装')
169
+ assert.equal(kindLabel('MissingSecret'), '缺密钥')
170
+ assert.equal(severityLabel('error'), '错误')
171
+ assert.equal(severityLabel('warning'), '警告')
172
+ assert.equal(severityLabel('info'), '信息')
173
+ })
174
+
175
+ /* ---------------------------------------------------------------- push 报告渲染 */
176
+
177
+ test('sync-view: push 成功报告 → ok 头部含快照 id + 分区透传', () => {
178
+ const report: SyncPushReport = { ok: true, snapshotId: 'sync-1', sections: ['settings', 'plugins'], warnings: [] }
179
+ const view = pushReportView(report)
180
+ assert.notEqual(view, null)
181
+ assert.equal(view?.kind, 'ok')
182
+ assert.match(view?.headline ?? '', /sync-1/)
183
+ assert.deepEqual(view?.sections, ['settings', 'plugins'])
184
+ })
185
+
186
+ test('sync-view: push 失败报告 → error 显示引擎 message', () => {
187
+ const report: SyncPushReport = { ok: false, snapshotId: '', sections: [], warnings: [], message: '全部导出失败' }
188
+ const view = pushReportView(report)
189
+ assert.equal(view?.kind, 'error')
190
+ assert.equal(view?.headline, '全部导出失败')
191
+ })
192
+
193
+ test('sync-view: null 报告 → null(不渲染卡片)', () => {
194
+ assert.equal(pushReportView(null), null)
195
+ assert.equal(pullReportView(null), null)
196
+ })
197
+
198
+ /* ---------------------------------------------------------------- pull 报告渲染 */
199
+
200
+ test('sync-view: pull 无变更 → empty 渲染(无差异列表)', () => {
201
+ const report: SyncPullReport = { ok: true, snapshotId: 'sync-1', changes: [], needsReview: false }
202
+ const view = pullReportView(report)
203
+ assert.equal(view?.kind, 'empty')
204
+ assert.equal(view?.summary, null)
205
+ })
206
+
207
+ test('sync-view: pull 差异报告 → ok + 摘要计数 + needsReview + 只读预览提示', () => {
208
+ const report: SyncPullReport = {
209
+ ok: true,
210
+ snapshotId: 'sync-9',
211
+ changes: [
212
+ change({ id: 'settings:a', kind: 'Update', description: '更新设置 a', severity: 'info' }),
213
+ change({ id: 'plugin:x', kind: 'Conflict', adapter: 'plugins', description: '插件 x 冲突', severity: 'warning' }),
214
+ ],
215
+ needsReview: true,
216
+ }
217
+ const view = pullReportView(report)
218
+ assert.equal(view?.kind, 'ok')
219
+ assert.match(view?.headline ?? '', /sync-9/)
220
+ assert.equal(view?.summary?.total, 2)
221
+ assert.equal(view?.summary?.needsReview, true)
222
+ assert.equal(view?.summary?.items[0]?.description, '更新设置 a')
223
+ assert.equal(view?.summary?.items[1]?.kind, 'Conflict')
224
+ assert.notEqual(view?.previewHint, '')
225
+ assert.match(view?.previewHint, /不会执行导入/)
226
+ })
227
+
228
+ test('sync-view: pull 失败 → error 渲染', () => {
229
+ const report: SyncPullReport = { ok: false, snapshotId: '', changes: [], needsReview: false, message: '认证失败' }
230
+ const view = pullReportView(report)
231
+ assert.equal(view?.kind, 'error')
232
+ assert.equal(view?.headline, '认证失败')
233
+ })
234
+
235
+ /* ---------------------------------------------------------------- 状态行 */
236
+
237
+ test('sync-view: 加载中 → loading 状态', () => {
238
+ const s = computeSyncStatus(null, true, null)
239
+ assert.equal(s.kind, 'loading')
240
+ })
241
+
242
+ test('sync-view: 加载失败 → error 状态带消息', () => {
243
+ const s = computeSyncStatus(null, false, '网络错误')
244
+ assert.equal(s.kind, 'error')
245
+ assert.equal(s.text, '网络错误')
246
+ })
247
+
248
+ test('sync-view: 未配置仓库 → unconfigured 提示', () => {
249
+ const s = computeSyncStatus(null, false, null)
250
+ assert.equal(s.kind, 'unconfigured')
251
+ assert.match(s.text, /尚未配置/)
252
+ })
253
+
254
+ test('sync-view: 已配置但缺凭据 → ready 文案提示未配置凭据', () => {
255
+ const info: SyncStatusResponse = {
256
+ ok: true, configured: true, repoUrl: 'https://github.com/u/r.git',
257
+ credentialConfigured: false, credentialWritable: true, lastSyncAt: undefined, sectionCount: 0,
258
+ }
259
+ const s = computeSyncStatus(info, false, null)
260
+ assert.equal(s.kind, 'ready')
261
+ assert.match(s.text, /未配置凭据/)
262
+ })
263
+
264
+ test('sync-view: 已配置 + 凭据就绪 + 上次同步 → ready 文案含日期与通道', () => {
265
+ const info: SyncStatusResponse = {
266
+ ok: true, configured: true, repoUrl: 'https://github.com/u/r.git',
267
+ credentialConfigured: true, credentialWritable: true,
268
+ lastSyncAt: '2026-08-16T10:30:00.000Z', sectionCount: 3,
269
+ transport: { type: 'git', ref: 'main' },
270
+ }
271
+ const s = computeSyncStatus(info, false, null)
272
+ assert.equal(s.kind, 'ready')
273
+ assert.match(s.text, /凭据已配置/)
274
+ assert.match(s.text, /上次同步/)
275
+ assert.match(s.text, /git\/main/)
276
+ })
277
+
278
+ test('sync-view: webdav 通道 → ready 文案回显通道类型(webdav + 服务器地址 ref)', () => {
279
+ const info: SyncStatusResponse = {
280
+ ok: true, configured: true, repoUrl: undefined,
281
+ credentialConfigured: false, credentialWritable: true,
282
+ webdav: { url: 'https://dav.example.com/dav/config', usernameConfigured: true, passwordConfigured: true },
283
+ lastSyncAt: '2026-08-16T10:30:00.000Z', sectionCount: 2,
284
+ transport: { type: 'webdav', ref: 'https://dav.example.com/dav/config' },
285
+ }
286
+ const s = computeSyncStatus(info, false, null)
287
+ assert.equal(s.kind, 'ready')
288
+ assert.match(s.text, /凭据已配置/)
289
+ assert.match(s.text, /webdav/)
290
+ assert.match(s.text, /https:\/\/dav\.example\.com\/dav\/config/)
291
+ })
292
+
293
+ /* ---------------------------------------------------------------- 时间格式化 */
294
+
295
+ test('sync-view: formatLastSync 空值 → 从未同步', () => {
296
+ assert.equal(formatLastSync(undefined), '从未同步')
297
+ assert.equal(formatLastSync(''), '从未同步')
298
+ })
299
+
300
+ test('sync-view: formatLastSync / formatDateTime 合法 ISO → 本地可读格式', () => {
301
+ const text = formatLastSync('2026-08-16T10:30:00.000Z')
302
+ assert.match(text, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
303
+ assert.equal(formatDateTime('not-a-date'), 'not-a-date')
304
+ })
305
+
306
+ /* ------------------------------------------------ GitHub 登录视图模型 */
307
+
308
+ test('sync-view: github 登录 idle → 可开始、不展示代码区块、无错误', () => {
309
+ const v = computeGithubLoginView('idle', '', '', null)
310
+ assert.equal(v.phase, 'idle')
311
+ assert.equal(v.canStart, true)
312
+ assert.equal(v.canCancel, false)
313
+ assert.equal(v.showCode, false)
314
+ assert.match(v.startLabel, /GitHub/)
315
+ })
316
+
317
+ test('sync-view: github 登录 starting → 不可重复发起、显示进行中文案', () => {
318
+ const v = computeGithubLoginView('starting', '', '', null)
319
+ assert.equal(v.canStart, false)
320
+ assert.equal(v.canCancel, true)
321
+ assert.match(v.statusText, /发起/)
322
+ })
323
+
324
+ test('sync-view: github 登录 waiting → 展示设备码与授权链接、可取消', () => {
325
+ const v = computeGithubLoginView('waiting', 'ABCD-EFGH', 'https://github.com/login/device', null)
326
+ assert.equal(v.showCode, true)
327
+ assert.equal(v.canCancel, true)
328
+ assert.equal(v.userCode, 'ABCD-EFGH')
329
+ assert.equal(v.verificationUri, 'https://github.com/login/device')
330
+ assert.match(v.statusText, /ABCD-EFGH/)
331
+ })
332
+
333
+ test('sync-view: github 登录 polling → 仍展示代码区块且可取消', () => {
334
+ const v = computeGithubLoginView('polling', 'ABCD-EFGH', 'https://github.com/login/device', null)
335
+ assert.equal(v.showCode, true)
336
+ assert.equal(v.canCancel, true)
337
+ assert.equal(v.canStart, false)
338
+ assert.match(v.statusText, /确认 GitHub 授权状态/)
339
+ })
340
+
341
+ test('sync-view: github 登录 success → 成功文案、代码区块隐藏', () => {
342
+ const v = computeGithubLoginView('success', 'ABCD-EFGH', 'https://github.com/login/device', null)
343
+ assert.equal(v.showCode, false)
344
+ assert.equal(v.canStart, false)
345
+ assert.match(v.statusText, /已安全写入/)
346
+ })
347
+
348
+ test('sync-view: github 登录 error → 展示错误 + 重新登录入口', () => {
349
+ const v = computeGithubLoginView('error', '', '', '授权被拒绝')
350
+ assert.equal(v.canStart, true)
351
+ assert.equal(v.canCancel, false)
352
+ assert.equal(v.error, '授权被拒绝')
353
+ assert.match(v.statusText, /授权被拒绝/)
354
+ assert.match(v.startLabel, /重新登录/)
355
+ })
356
+
357
+ test('sync-view: githubPollMessage 映射轮询终止态(denied/expired/error/success)', () => {
358
+ const denied: GithubPollResponse = { status: 'denied' }
359
+ assert.match(githubPollMessage(denied), /拒绝/)
360
+ const expired: GithubPollResponse = { status: 'expired' }
361
+ assert.match(githubPollMessage(expired), /过期/)
362
+ const error: GithubPollResponse = { status: 'error', errorCode: 'incorrect_device_code', message: '设备码不匹配' }
363
+ assert.match(githubPollMessage(error), /设备码不匹配/)
364
+ const success: GithubPollResponse = { status: 'success', credentialConfigured: true }
365
+ assert.match(githubPollMessage(success), /已安全写入/)
366
+ const pending: GithubPollResponse = { status: 'pending', pollDelayMs: 5000 }
367
+ assert.equal(githubPollMessage(pending), '', 'pending 不是终止态,不应产生消息')
368
+ })
369
+
370
+ /* ---------------------------------------------------------------- 自动同步倒计时 */
371
+
372
+ test('sync-view: autosyncIntervalMs 各档位换算正确', () => {
373
+ assert.equal(autosyncIntervalMs('5m'), 5 * 60 * 1000)
374
+ assert.equal(autosyncIntervalMs('15m'), 15 * 60 * 1000)
375
+ assert.equal(autosyncIntervalMs('30m'), 30 * 60 * 1000)
376
+ assert.equal(autosyncIntervalMs('60m'), 60 * 60 * 1000)
377
+ assert.equal(autosyncIntervalMs('6h'), 6 * 60 * 60 * 1000)
378
+ assert.equal(autosyncIntervalMs('12h'), 12 * 60 * 60 * 1000)
379
+ assert.equal(autosyncIntervalMs('24h'), 24 * 60 * 60 * 1000)
380
+ })
381
+
382
+ test('sync-view: computeAutosyncCountdown 已到期 → 0;未到 → interval - elapsed;从未运行 → -1', () => {
383
+ assert.equal(computeAutosyncCountdown(0, 5 * 60 * 1000), 5 * 60 * 1000)
384
+ assert.equal(computeAutosyncCountdown(60 * 1000, 5 * 60 * 1000), 4 * 60 * 1000)
385
+ assert.equal(computeAutosyncCountdown(5 * 60 * 1000, 5 * 60 * 1000), 0)
386
+ assert.equal(computeAutosyncCountdown(6 * 60 * 1000, 5 * 60 * 1000), 0)
387
+ assert.equal(computeAutosyncCountdown(-1, 5 * 60 * 1000), -1)
388
+ })
389
+
390
+ test('sync-view: formatIntervalDuration 输出真实剩余时长(不再把 <1 小时一律显示成 30 分钟)', () => {
391
+ // 回归:旧实现 `if (minutes < 60) return '30 分钟'` 导致 5m 间隔永远显示「约 30 分钟后」
392
+ assert.equal(formatIntervalDuration(4 * 60 * 1000), '4 分钟')
393
+ assert.equal(formatIntervalDuration(5 * 60 * 1000), '5 分钟')
394
+ assert.equal(formatIntervalDuration(30 * 60 * 1000), '30 分钟')
395
+ // 整 60 分钟进位为 1 小时
396
+ assert.equal(formatIntervalDuration(60 * 60 * 1000), '1 小时')
397
+ // 向上取整:90 分钟 → 2 小时;6 小时整 → 6 小时
398
+ assert.equal(formatIntervalDuration(90 * 60 * 1000), '2 小时')
399
+ assert.equal(formatIntervalDuration(6 * 60 * 60 * 1000), '6 小时')
400
+ // 跨天:24 小时 → 1 天;48 小时 → 2 天
401
+ assert.equal(formatIntervalDuration(24 * 60 * 60 * 1000), '1 天')
402
+ assert.equal(formatIntervalDuration(48 * 60 * 60 * 1000), '2 天')
403
+ // 已到期/异常值兜底为 1 分钟,不出现 0 分钟
404
+ assert.equal(formatIntervalDuration(0), '1 分钟')
405
+ })
406
+
407
+ /* ---------------------------------------------------------------- WebDAV 预设 */
408
+
409
+ test('sync-view: WEBDAV_PRESETS 含自定义与 6 个常见服务器,首项为自定义', () => {
410
+ assert.equal(WEBDAV_PRESETS[0]?.id, 'custom')
411
+ assert.ok(WEBDAV_PRESETS.length >= 6)
412
+ const ids = WEBDAV_PRESETS.map((p) => p.id)
413
+ assert.ok(ids.includes('jianguoyun'))
414
+ assert.ok(ids.includes('nextcloud'))
415
+ assert.ok(ids.includes('box'))
416
+ })
417
+
418
+ test('sync-view: presetById 已知 id → 对应预设;未知 → 自定义兜底', () => {
419
+ assert.equal(presetById('jianguoyun').url, 'https://dav.jianguoyun.com/dav/')
420
+ assert.equal(presetById('nextcloud').url, 'https://<server>/remote.php/dav/files/<user>/')
421
+ assert.equal(presetById('unknown-xyz').id, 'custom', '未知 id 回退自定义')
422
+ })
423
+
424
+ test('sync-view: presetIdForUrl 空/自定义 → custom;匹配常见前缀 → 对应预设', () => {
425
+ assert.equal(presetIdForUrl(''), 'custom')
426
+ assert.equal(presetIdForUrl('https://anything.example/x'), 'custom')
427
+ assert.equal(presetIdForUrl('https://dav.jianguoyun.com/dav/'), 'jianguoyun')
428
+ assert.equal(presetIdForUrl('https://dav.box.com/dav/my-config/'), 'box')
429
+ })
430
+
431
+ /* ---------------------------------------------------------------- 通道选择持久化 */
432
+
433
+ /** 内存 mock storage(node 无 localStorage) */
434
+ function makeStorage(): { getItem(k: string): string | null; setItem(k: string, v: string): void } {
435
+ const map = new Map<string, string>()
436
+ return {
437
+ getItem: (k) => map.get(k) ?? null,
438
+ setItem: (k, v) => { map.set(k, v) },
439
+ }
440
+ }
441
+
442
+ test('sync-view: readStoredChannel 无值/非法 → null;git/webdav → 对应通道', () => {
443
+ const s = makeStorage()
444
+ assert.equal(readStoredChannel(s), null, '无记录 → null')
445
+ s.setItem('dsh.configManager.syncChannel', 'webdav')
446
+ assert.equal(readStoredChannel(s), 'webdav')
447
+ s.setItem('dsh.configManager.syncChannel', 'git')
448
+ assert.equal(readStoredChannel(s), 'git')
449
+ s.setItem('dsh.configManager.syncChannel', 'garbage')
450
+ assert.equal(readStoredChannel(s), null, '非法值 → null')
451
+ })
452
+
453
+ test('sync-view: writeStoredChannel 写回 localStorage,可被 readStoredChannel 读回', () => {
454
+ const s = makeStorage()
455
+ writeStoredChannel('webdav', s)
456
+ assert.equal(readStoredChannel(s), 'webdav')
457
+ writeStoredChannel('git', s)
458
+ assert.equal(readStoredChannel(s), 'git')
459
+ })
460
+
461
+ /* ---------------------------------------------------------------- 通道子 tab(每通道独立) */
462
+
463
+ test('sync-view: channelTabModels git 激活 → git active / webdav 未激活;busy 全禁用', () => {
464
+ const tabs = channelTabModels('git', false)
465
+ assert.equal(tabs.length, 2)
466
+ assert.deepEqual(tabs[0], { channel: 'git', active: true, disabled: false })
467
+ assert.deepEqual(tabs[1], { channel: 'webdav', active: false, disabled: false })
468
+ const busy = channelTabModels('webdav', true)
469
+ assert.equal(busy[0]?.active, false)
470
+ assert.equal(busy[1]?.active, true)
471
+ assert.ok(busy.every((b) => b.disabled), 'busy 时两个子 tab 都禁用(防并发操作切换)')
472
+ })
473
+
474
+ test('sync-view: defaultChannelSyncState 每通道独立缺省值', () => {
475
+ const git = defaultChannelSyncState()
476
+ assert.equal(git.syncMode, 'default')
477
+ assert.deepEqual(git.syncSections, [])
478
+ assert.equal(git.selectedSnapshotId, '')
479
+ assert.deepEqual(git.snapshots, [])
480
+ assert.equal(git.autosync, null)
481
+ assert.equal(git.autosyncEnabled, false)
482
+ assert.equal(git.autosyncInterval, '30m')
483
+ // 两通道缺省互不影响(同一默认工厂,调用即得独立实例)
484
+ const webdav = defaultChannelSyncState()
485
+ webdav.syncMode = 'advanced'
486
+ assert.equal(git.syncMode, 'default', '修改一个通道缺省不影响另一个')
487
+ })