@taskforcehq/taskforce 0.3.168

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 (492) hide show
  1. package/README.md +241 -0
  2. package/dist/Taskforce.d.ts +3 -0
  3. package/dist/Taskforce.js +3 -0
  4. package/dist/Taskforce.module.css +7587 -0
  5. package/dist/TaskforceCore.CategoryFallback.test.d.ts +1 -0
  6. package/dist/TaskforceCore.CategoryFallback.test.js +98 -0
  7. package/dist/TaskforceCore.d.ts +12 -0
  8. package/dist/TaskforceCore.js +729 -0
  9. package/dist/TaskforceCore.test.d.ts +1 -0
  10. package/dist/TaskforceCore.test.js +2132 -0
  11. package/dist/TaskforceScreenshot.d.ts +11 -0
  12. package/dist/TaskforceScreenshot.js +113 -0
  13. package/dist/TaskforceWidget.d.ts +6 -0
  14. package/dist/TaskforceWidget.js +101 -0
  15. package/dist/TaskforceWidget.test.d.ts +1 -0
  16. package/dist/TaskforceWidget.test.js +92 -0
  17. package/dist/UnsavedChangesRelaxed.test.d.ts +1 -0
  18. package/dist/UnsavedChangesRelaxed.test.js +16 -0
  19. package/dist/__tests__/localizationCatalogs.test.d.ts +1 -0
  20. package/dist/__tests__/localizationCatalogs.test.js +37 -0
  21. package/dist/__tests__/planParser.test.d.ts +1 -0
  22. package/dist/__tests__/planParser.test.js +183 -0
  23. package/dist/__tests__/releaseVersion.test.d.ts +1 -0
  24. package/dist/__tests__/releaseVersion.test.js +32 -0
  25. package/dist/__tests__/structuredCommentValidation.test.d.ts +1 -0
  26. package/dist/__tests__/structuredCommentValidation.test.js +31 -0
  27. package/dist/__tests__/structuredDocValidation.test.d.ts +1 -0
  28. package/dist/__tests__/structuredDocValidation.test.js +30 -0
  29. package/dist/__tests__/templateEngine.test.d.ts +1 -0
  30. package/dist/__tests__/templateEngine.test.js +150 -0
  31. package/dist/__tests__/templates.test.d.ts +1 -0
  32. package/dist/__tests__/templates.test.js +45 -0
  33. package/dist/__tests__/workflowDocumentFixtures.test.d.ts +1 -0
  34. package/dist/__tests__/workflowDocumentFixtures.test.js +78 -0
  35. package/dist/__tests__/workflowExportMatrix.test.d.ts +1 -0
  36. package/dist/__tests__/workflowExportMatrix.test.js +113 -0
  37. package/dist/__tests__/workflowExportSnapshots.test.d.ts +1 -0
  38. package/dist/__tests__/workflowExportSnapshots.test.js +28 -0
  39. package/dist/__tests__/workflowGeneration.test.d.ts +1 -0
  40. package/dist/__tests__/workflowGeneration.test.js +12 -0
  41. package/dist/__tests__/workflowSimulation.test.d.ts +1 -0
  42. package/dist/__tests__/workflowSimulation.test.js +113 -0
  43. package/dist/__tests__/workflowV2Policy.test.d.ts +1 -0
  44. package/dist/__tests__/workflowV2Policy.test.js +80 -0
  45. package/dist/api.d.ts +30 -0
  46. package/dist/api.js +85 -0
  47. package/dist/cli.d.ts +2 -0
  48. package/dist/cli.js +217 -0
  49. package/dist/compat/priorityCompat.d.ts +2 -0
  50. package/dist/compat/priorityCompat.js +24 -0
  51. package/dist/components/features/DocumentGenerateModal.d.ts +9 -0
  52. package/dist/components/features/DocumentGenerateModal.js +114 -0
  53. package/dist/components/features/DocumentIndex.d.ts +19 -0
  54. package/dist/components/features/DocumentIndex.js +51 -0
  55. package/dist/components/features/DocumentViewer.d.ts +9 -0
  56. package/dist/components/features/DocumentViewer.js +253 -0
  57. package/dist/components/features/DocumentWorkspace.d.ts +21 -0
  58. package/dist/components/features/DocumentWorkspace.js +88 -0
  59. package/dist/components/features/TaskSettings.d.ts +209 -0
  60. package/dist/components/features/TaskSettings.js +500 -0
  61. package/dist/components/features/TaskSettings.test.d.ts +1 -0
  62. package/dist/components/features/TaskSettings.test.js +308 -0
  63. package/dist/components/features/settings/CategoryManager.d.ts +19 -0
  64. package/dist/components/features/settings/CategoryManager.js +113 -0
  65. package/dist/components/features/settings/TaxonomyEditor.d.ts +8 -0
  66. package/dist/components/features/settings/TaxonomyEditor.js +98 -0
  67. package/dist/components/features/settings/TaxonomyManager 2.d.ts +52 -0
  68. package/dist/components/features/settings/TaxonomyManager 2.js +75 -0
  69. package/dist/components/features/settings/TaxonomyManager.d.ts +54 -0
  70. package/dist/components/features/settings/TaxonomyManager.js +77 -0
  71. package/dist/components/features/settings/TypeManager.d.ts +10 -0
  72. package/dist/components/features/settings/TypeManager.js +50 -0
  73. package/dist/components/features/settings/WorkflowEditorPrototype.d.ts +15 -0
  74. package/dist/components/features/settings/WorkflowEditorPrototype.js +245 -0
  75. package/dist/components/task/TaskActionHeader.d.ts +20 -0
  76. package/dist/components/task/TaskActionHeader.js +13 -0
  77. package/dist/components/task/TaskCard.d.ts +47 -0
  78. package/dist/components/task/TaskCard.js +326 -0
  79. package/dist/components/task/TaskCard.test.d.ts +1 -0
  80. package/dist/components/task/TaskCard.test.js +95 -0
  81. package/dist/components/task/TaskContextUpload.d.ts +12 -0
  82. package/dist/components/task/TaskContextUpload.js +393 -0
  83. package/dist/components/task/TaskContextUpload.test.d.ts +1 -0
  84. package/dist/components/task/TaskContextUpload.test.js +189 -0
  85. package/dist/components/task/TaskForm.d.ts +105 -0
  86. package/dist/components/task/TaskForm.js +241 -0
  87. package/dist/components/task/TaskForm.test.d.ts +1 -0
  88. package/dist/components/task/TaskForm.test.js +288 -0
  89. package/dist/components/task/TaskKanban 2.d.ts +76 -0
  90. package/dist/components/task/TaskKanban 2.js +1004 -0
  91. package/dist/components/task/TaskKanban.d.ts +76 -0
  92. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +1 -0
  93. package/dist/components/task/TaskKanban.empty-column-drop.test.js +66 -0
  94. package/dist/components/task/TaskKanban.js +1026 -0
  95. package/dist/components/task/TaskKanban.test.d.ts +1 -0
  96. package/dist/components/task/TaskKanban.test.js +46 -0
  97. package/dist/components/task/TaskList.d.ts +74 -0
  98. package/dist/components/task/TaskList.js +38 -0
  99. package/dist/components/task/TaskList.test.d.ts +1 -0
  100. package/dist/components/task/TaskList.test.js +270 -0
  101. package/dist/components/task/TaskScreenshotSection.d.ts +15 -0
  102. package/dist/components/task/TaskScreenshotSection.js +22 -0
  103. package/dist/components/ui/LevelSelector.d.ts +13 -0
  104. package/dist/components/ui/LevelSelector.js +25 -0
  105. package/dist/components/ui/LevelSelector.test.d.ts +1 -0
  106. package/dist/components/ui/LevelSelector.test.js +55 -0
  107. package/dist/components/ui/Markdown.d.ts +6 -0
  108. package/dist/components/ui/Markdown.js +94 -0
  109. package/dist/components/ui/Markdown.test.d.ts +1 -0
  110. package/dist/components/ui/Markdown.test.js +38 -0
  111. package/dist/components/ui/Modal.d.ts +16 -0
  112. package/dist/components/ui/Modal.js +49 -0
  113. package/dist/components/ui/MultiSelectFilter.d.ts +10 -0
  114. package/dist/components/ui/MultiSelectFilter.js +71 -0
  115. package/dist/components/ui/MultiSelectFilter.test.d.ts +1 -0
  116. package/dist/components/ui/MultiSelectFilter.test.js +74 -0
  117. package/dist/components/ui/TaxonomyDropdown.d.ts +17 -0
  118. package/dist/components/ui/TaxonomyDropdown.js +145 -0
  119. package/dist/components/ui/TaxonomyDropdown.test.d.ts +1 -0
  120. package/dist/components/ui/TaxonomyDropdown.test.js +130 -0
  121. package/dist/components/views/StandaloneLayout 2.d.ts +8 -0
  122. package/dist/components/views/StandaloneLayout 2.js +1660 -0
  123. package/dist/components/views/StandaloneLayout.d.ts +8 -0
  124. package/dist/components/views/StandaloneLayout.js +2117 -0
  125. package/dist/components/views/WidgetView.d.ts +8 -0
  126. package/dist/components/views/WidgetView.js +221 -0
  127. package/dist/config/envSchema.d.ts +28 -0
  128. package/dist/config/envSchema.js +374 -0
  129. package/dist/config/envSchema.test.d.ts +1 -0
  130. package/dist/config/envSchema.test.js +97 -0
  131. package/dist/config/surfaceBase.d.ts +2 -0
  132. package/dist/config/surfaceBase.js +8 -0
  133. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +1 -0
  134. package/dist/core/BulkUpdateFieldsPreview.test.js +62 -0
  135. package/dist/core/CompletedAt.test.d.ts +1 -0
  136. package/dist/core/CompletedAt.test.js +44 -0
  137. package/dist/core/CreatedBy.test.d.ts +1 -0
  138. package/dist/core/CreatedBy.test.js +60 -0
  139. package/dist/core/EntitlementsPolicy.test.d.ts +1 -0
  140. package/dist/core/EntitlementsPolicy.test.js +145 -0
  141. package/dist/core/InitiativeTemplates.test.d.ts +1 -0
  142. package/dist/core/InitiativeTemplates.test.js +50 -0
  143. package/dist/core/ParentLifecycle.test.d.ts +1 -0
  144. package/dist/core/ParentLifecycle.test.js +98 -0
  145. package/dist/core/ScheduleFields.test.d.ts +1 -0
  146. package/dist/core/ScheduleFields.test.js +197 -0
  147. package/dist/core/SetupState.test.d.ts +1 -0
  148. package/dist/core/SetupState.test.js +113 -0
  149. package/dist/core/SystemAdmin.test.d.ts +1 -0
  150. package/dist/core/SystemAdmin.test.js +295 -0
  151. package/dist/core/TaskTaxonomyValidation.test.d.ts +1 -0
  152. package/dist/core/TaskTaxonomyValidation.test.js +64 -0
  153. package/dist/core/Taskforce.d.ts +1063 -0
  154. package/dist/core/Taskforce.ids.test.d.ts +1 -0
  155. package/dist/core/Taskforce.ids.test.js +168 -0
  156. package/dist/core/Taskforce.js +5981 -0
  157. package/dist/core/Taskforce.realtimeMutation.test.d.ts +1 -0
  158. package/dist/core/Taskforce.realtimeMutation.test.js +76 -0
  159. package/dist/core/WorkspaceDelete.test.d.ts +1 -0
  160. package/dist/core/WorkspaceDelete.test.js +140 -0
  161. package/dist/core/WorkspacePermissions.test.d.ts +1 -0
  162. package/dist/core/WorkspacePermissions.test.js +151 -0
  163. package/dist/core/WorkspacePlanMode.test.d.ts +1 -0
  164. package/dist/core/WorkspacePlanMode.test.js +104 -0
  165. package/dist/hooks/authGuardPolicy.d.ts +26 -0
  166. package/dist/hooks/authGuardPolicy.js +32 -0
  167. package/dist/hooks/authGuardPolicy.test.d.ts +1 -0
  168. package/dist/hooks/authGuardPolicy.test.js +76 -0
  169. package/dist/hooks/useDraggable.d.ts +12 -0
  170. package/dist/hooks/useDraggable.js +65 -0
  171. package/dist/hooks/useRealtimeSync.d.ts +24 -0
  172. package/dist/hooks/useRealtimeSync.js +275 -0
  173. package/dist/hooks/useRealtimeSync.test.d.ts +1 -0
  174. package/dist/hooks/useRealtimeSync.test.js +284 -0
  175. package/dist/hooks/useSyncOrchestrator.d.ts +164 -0
  176. package/dist/hooks/useSyncOrchestrator.js +1427 -0
  177. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +1 -0
  178. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +367 -0
  179. package/dist/hooks/useTaskforce.d.ts +727 -0
  180. package/dist/hooks/useTaskforce.js +4396 -0
  181. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +1 -0
  182. package/dist/hooks/useTaskforce.priority-filters.test.js +33 -0
  183. package/dist/hooks/useWorkspaceSyncController.d.ts +80 -0
  184. package/dist/hooks/useWorkspaceSyncController.js +598 -0
  185. package/dist/hooks/useWorkspaceSyncController.test.d.ts +1 -0
  186. package/dist/hooks/useWorkspaceSyncController.test.js +236 -0
  187. package/dist/index.d.ts +12 -0
  188. package/dist/index.js +11 -0
  189. package/dist/localization/index.d.ts +8 -0
  190. package/dist/localization/index.js +81 -0
  191. package/dist/localization/locales/en-US.d.ts +151 -0
  192. package/dist/localization/locales/en-US.js +150 -0
  193. package/dist/localization/locales/es-419.d.ts +2 -0
  194. package/dist/localization/locales/es-419.js +150 -0
  195. package/dist/localization/locales/pt-BR.d.ts +2 -0
  196. package/dist/localization/locales/pt-BR.js +150 -0
  197. package/dist/main.d.ts +1 -0
  198. package/dist/main.js +33 -0
  199. package/dist/mcp/optionResolution.d.ts +14 -0
  200. package/dist/mcp/optionResolution.js +126 -0
  201. package/dist/mcp/optionResolution.test.d.ts +1 -0
  202. package/dist/mcp/optionResolution.test.js +39 -0
  203. package/dist/mcp/server.d.ts +1 -0
  204. package/dist/mcp/server.js +1793 -0
  205. package/dist/mcp/structuredCommentValidation.d.ts +6 -0
  206. package/dist/mcp/structuredCommentValidation.js +56 -0
  207. package/dist/mcp/structuredDocValidation.d.ts +6 -0
  208. package/dist/mcp/structuredDocValidation.js +75 -0
  209. package/dist/migrations/billingSchemaParity.test.d.ts +1 -0
  210. package/dist/migrations/billingSchemaParity.test.js +46 -0
  211. package/dist/migrations/clientMigrations.d.ts +9 -0
  212. package/dist/migrations/clientMigrations.js +74 -0
  213. package/dist/migrations/flags.d.ts +6 -0
  214. package/dist/migrations/flags.js +8 -0
  215. package/dist/migrations/taskMigrations.d.ts +71 -0
  216. package/dist/migrations/taskMigrations.js +236 -0
  217. package/dist/migrations/taskMigrations.test.d.ts +1 -0
  218. package/dist/migrations/taskMigrations.test.js +34 -0
  219. package/dist/migrations/taskSchemaMigrations.d.ts +11 -0
  220. package/dist/migrations/taskSchemaMigrations.js +453 -0
  221. package/dist/plugins/index.d.ts +10 -0
  222. package/dist/plugins/index.js +10 -0
  223. package/dist/plugins/vite.d.ts +19 -0
  224. package/dist/plugins/vite.js +271 -0
  225. package/dist/public/android-chrome-192x192.png +0 -0
  226. package/dist/public/android-chrome-512x512.png +0 -0
  227. package/dist/public/apple-touch-icon.png +0 -0
  228. package/dist/public/favicon-16x16.png +0 -0
  229. package/dist/public/favicon-32x32.png +0 -0
  230. package/dist/public/favicon.ico +0 -0
  231. package/dist/public/favicon.png +0 -0
  232. package/dist/public/site.webmanifest +1 -0
  233. package/dist/public/taskforce.png +0 -0
  234. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  235. package/dist/resources/templates/environments/aider.yaml +14 -0
  236. package/dist/resources/templates/environments/antigravity.yaml +10 -0
  237. package/dist/resources/templates/environments/claude.yaml +10 -0
  238. package/dist/resources/templates/environments/cline.yaml +10 -0
  239. package/dist/resources/templates/environments/codex.yaml +13 -0
  240. package/dist/resources/templates/environments/copilot.yaml +10 -0
  241. package/dist/resources/templates/environments/cursor.yaml +9 -0
  242. package/dist/resources/templates/environments/gemini.yaml +9 -0
  243. package/dist/resources/templates/environments/windsurf.yaml +10 -0
  244. package/dist/resources/templates/workflow-sources/README.md +9 -0
  245. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  246. package/dist/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  247. package/dist/resources/templates/workflows/collaborate.yaml +104 -0
  248. package/dist/resources/templates/workflows/do.yaml +82 -0
  249. package/dist/resources/templates/workflows/evaluate.yaml +111 -0
  250. package/dist/resources/templates/workflows/execute.yaml +142 -0
  251. package/dist/resources/templates/workflows/plan.yaml +112 -0
  252. package/dist/resources/templates/workflows/review.yaml +109 -0
  253. package/dist/runtime/capabilities.d.ts +8 -0
  254. package/dist/runtime/capabilities.js +16 -0
  255. package/dist/security/virusScan.d.ts +16 -0
  256. package/dist/security/virusScan.js +83 -0
  257. package/dist/server/auth.d.ts +51 -0
  258. package/dist/server/auth.js +266 -0
  259. package/dist/server/auth.test.d.ts +1 -0
  260. package/dist/server/auth.test.js +111 -0
  261. package/dist/server/buildInfo.d.ts +7 -0
  262. package/dist/server/buildInfo.js +100 -0
  263. package/dist/server/buildInfo.test.d.ts +1 -0
  264. package/dist/server/buildInfo.test.js +29 -0
  265. package/dist/server/cors.d.ts +7 -0
  266. package/dist/server/cors.js +76 -0
  267. package/dist/server/cors.test.d.ts +1 -0
  268. package/dist/server/cors.test.js +71 -0
  269. package/dist/server/email.d.ts +28 -0
  270. package/dist/server/email.js +138 -0
  271. package/dist/server/email.test.d.ts +1 -0
  272. package/dist/server/email.test.js +113 -0
  273. package/dist/server/index.cookieProxy.test.d.ts +1 -0
  274. package/dist/server/index.cookieProxy.test.js +19 -0
  275. package/dist/server/index.d.ts +17 -0
  276. package/dist/server/index.js +757 -0
  277. package/dist/server/index.rateLimit.test.d.ts +1 -0
  278. package/dist/server/index.rateLimit.test.js +13 -0
  279. package/dist/server/index.test.d.ts +1 -0
  280. package/dist/server/index.test.js +19 -0
  281. package/dist/server/rateLimit.d.ts +29 -0
  282. package/dist/server/rateLimit.js +99 -0
  283. package/dist/server/rateLimit.test.d.ts +1 -0
  284. package/dist/server/rateLimit.test.js +107 -0
  285. package/dist/server/realtimeSync.auth.test.d.ts +1 -0
  286. package/dist/server/realtimeSync.auth.test.js +62 -0
  287. package/dist/server/realtimeSync.replay.test.d.ts +1 -0
  288. package/dist/server/realtimeSync.replay.test.js +192 -0
  289. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +1 -0
  290. package/dist/server/realtimeSync.workspaceIsolation.test.js +101 -0
  291. package/dist/server/realtimeSyncWs.d.ts +30 -0
  292. package/dist/server/realtimeSyncWs.js +257 -0
  293. package/dist/server/routes.billing.d.ts +15 -0
  294. package/dist/server/routes.billing.js +991 -0
  295. package/dist/server/routes.billing.test.d.ts +1 -0
  296. package/dist/server/routes.billing.test.js +387 -0
  297. package/dist/server/routes.d.ts +72 -0
  298. package/dist/server/routes.js +4820 -0
  299. package/dist/server/routes.sync.d.ts +17 -0
  300. package/dist/server/routes.sync.integration.test.d.ts +1 -0
  301. package/dist/server/routes.sync.integration.test.js +470 -0
  302. package/dist/server/routes.sync.js +1141 -0
  303. package/dist/server/routes.test.d.ts +1 -0
  304. package/dist/server/routes.test.js +4945 -0
  305. package/dist/server/runtimeMode.d.ts +4 -0
  306. package/dist/server/runtimeMode.js +25 -0
  307. package/dist/server/runtimeMode.test.d.ts +1 -0
  308. package/dist/server/runtimeMode.test.js +26 -0
  309. package/dist/server/securityHeaders.d.ts +3 -0
  310. package/dist/server/securityHeaders.js +25 -0
  311. package/dist/server/securityHeaders.test.d.ts +1 -0
  312. package/dist/server/securityHeaders.test.js +32 -0
  313. package/dist/server/smtpTransport.d.ts +21 -0
  314. package/dist/server/smtpTransport.js +11 -0
  315. package/dist/server/start.d.ts +1 -0
  316. package/dist/server/start.js +17 -0
  317. package/dist/services/formatTransformers.d.ts +29 -0
  318. package/dist/services/formatTransformers.js +56 -0
  319. package/dist/services/templateEngine.d.ts +46 -0
  320. package/dist/services/templateEngine.js +148 -0
  321. package/dist/shared/runtimeContract.d.ts +12 -0
  322. package/dist/shared/runtimeContract.js +23 -0
  323. package/dist/shared/runtimeContract.test.d.ts +1 -0
  324. package/dist/shared/runtimeContract.test.js +23 -0
  325. package/dist/soak/preflight.cli.d.ts +1 -0
  326. package/dist/soak/preflight.cli.js +28 -0
  327. package/dist/soak/preflight.d.ts +26 -0
  328. package/dist/soak/preflight.js +113 -0
  329. package/dist/soak/preflight.test.d.ts +1 -0
  330. package/dist/soak/preflight.test.js +39 -0
  331. package/dist/soak/syncSoakHarness.d.ts +6 -0
  332. package/dist/soak/syncSoakHarness.js +10 -0
  333. package/dist/storage/createDatabaseAdapter.d.ts +2 -0
  334. package/dist/storage/createDatabaseAdapter.js +9 -0
  335. package/dist/storage/databaseAdapter.d.ts +22 -0
  336. package/dist/storage/databaseAdapter.js +1 -0
  337. package/dist/storage/documentIntegrity.d.ts +28 -0
  338. package/dist/storage/documentIntegrity.js +103 -0
  339. package/dist/storage/documentPurge.d.ts +14 -0
  340. package/dist/storage/documentPurge.js +46 -0
  341. package/dist/storage/documentRegistry.d.ts +39 -0
  342. package/dist/storage/documentRegistry.js +115 -0
  343. package/dist/storage/integritySchedule.d.ts +17 -0
  344. package/dist/storage/integritySchedule.js +62 -0
  345. package/dist/storage/objectStorage.d.ts +20 -0
  346. package/dist/storage/objectStorage.js +15 -0
  347. package/dist/storage/objectStorageClient.d.ts +18 -0
  348. package/dist/storage/objectStorageClient.js +301 -0
  349. package/dist/storage/objectStorageConfig.d.ts +2 -0
  350. package/dist/storage/objectStorageConfig.js +45 -0
  351. package/dist/storage/postgresAdapter.d.ts +20 -0
  352. package/dist/storage/postgresAdapter.js +204 -0
  353. package/dist/storage/postgresWorker.d.ts +1 -0
  354. package/dist/storage/postgresWorker.js +110 -0
  355. package/dist/storage/providerConfig.d.ts +6 -0
  356. package/dist/storage/providerConfig.js +11 -0
  357. package/dist/storage/sqliteAdapter.d.ts +14 -0
  358. package/dist/storage/sqliteAdapter.js +28 -0
  359. package/dist/storage/taskAssetStore.d.ts +56 -0
  360. package/dist/storage/taskAssetStore.js +297 -0
  361. package/dist/sync/cloudSyncApi.d.ts +47 -0
  362. package/dist/sync/cloudSyncApi.js +138 -0
  363. package/dist/sync/cloudSyncApi.test.d.ts +1 -0
  364. package/dist/sync/cloudSyncApi.test.js +54 -0
  365. package/dist/sync/encryptionService.d.ts +11 -0
  366. package/dist/sync/encryptionService.js +61 -0
  367. package/dist/sync/encryptionService.test.d.ts +1 -0
  368. package/dist/sync/encryptionService.test.js +52 -0
  369. package/dist/sync/realtimeFeature.d.ts +6 -0
  370. package/dist/sync/realtimeFeature.js +20 -0
  371. package/dist/sync/realtimeFeature.test.d.ts +1 -0
  372. package/dist/sync/realtimeFeature.test.js +30 -0
  373. package/dist/sync/realtimeReconcile.d.ts +30 -0
  374. package/dist/sync/realtimeReconcile.js +90 -0
  375. package/dist/sync/realtimeReconcile.test.d.ts +1 -0
  376. package/dist/sync/realtimeReconcile.test.js +43 -0
  377. package/dist/sync/syncService.d.ts +96 -0
  378. package/dist/sync/syncService.js +423 -0
  379. package/dist/sync/syncService.test.d.ts +1 -0
  380. package/dist/sync/syncService.test.js +484 -0
  381. package/dist/sync/workspaceSyncModel.d.ts +61 -0
  382. package/dist/sync/workspaceSyncModel.js +136 -0
  383. package/dist/sync/workspaceSyncModel.test.d.ts +1 -0
  384. package/dist/sync/workspaceSyncModel.test.js +73 -0
  385. package/dist/test/setup.d.ts +1 -0
  386. package/dist/test/setup.js +15 -0
  387. package/dist/types.d.ts +157 -0
  388. package/dist/types.js +41 -0
  389. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  390. package/dist/ui/assets/index-0W5vyCpn.css +1 -0
  391. package/dist/ui/assets/index-MCZuNNfn.js +23 -0
  392. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  393. package/dist/ui/assets/taskforce-BxLPokNB.png +0 -0
  394. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +5 -0
  395. package/dist/ui/assets/vendor-icons-ZFG3SGwo.js +1 -0
  396. package/dist/ui/assets/vendor-markdown-CKYkQHfj.js +29 -0
  397. package/dist/ui/assets/vendor-react-DdIDsh7f.js +9 -0
  398. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +3 -0
  399. package/dist/ui/branding/logos/logo_black_01.png +0 -0
  400. package/dist/ui/branding/logos/logo_black_02.png +0 -0
  401. package/dist/ui/branding/logos/logo_white_01.png +0 -0
  402. package/dist/ui/branding/logos/logo_white_02.png +0 -0
  403. package/dist/ui/favicon/android-chrome-192x192.png +0 -0
  404. package/dist/ui/favicon/android-chrome-512x512.png +0 -0
  405. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  406. package/dist/ui/favicon/favicon-16x16.png +0 -0
  407. package/dist/ui/favicon/favicon-32x32.png +0 -0
  408. package/dist/ui/favicon/favicon.ico +0 -0
  409. package/dist/ui/favicon/favicon.png +0 -0
  410. package/dist/ui/favicon/site.webmanifest +1 -0
  411. package/dist/ui/fonts/RussoOne-Regular.ttf +0 -0
  412. package/dist/ui/images/taskforce.png +0 -0
  413. package/dist/ui/index.html +19 -0
  414. package/dist/utils/checklistMarkdown.d.ts +9 -0
  415. package/dist/utils/checklistMarkdown.js +46 -0
  416. package/dist/utils/constants.d.ts +21 -0
  417. package/dist/utils/constants.icons.test.d.ts +1 -0
  418. package/dist/utils/constants.icons.test.js +40 -0
  419. package/dist/utils/constants.js +100 -0
  420. package/dist/utils/contextFiles.d.ts +8 -0
  421. package/dist/utils/contextFiles.js +54 -0
  422. package/dist/utils/formatting.d.ts +5 -0
  423. package/dist/utils/formatting.js +30 -0
  424. package/dist/utils/planParser.d.ts +22 -0
  425. package/dist/utils/planParser.js +151 -0
  426. package/dist/utils/taskDragZones.d.ts +8 -0
  427. package/dist/utils/taskDragZones.js +14 -0
  428. package/dist/utils/taskDragZones.test.d.ts +1 -0
  429. package/dist/utils/taskDragZones.test.js +14 -0
  430. package/dist/utils/taskHierarchy.d.ts +4 -0
  431. package/dist/utils/taskHierarchy.js +69 -0
  432. package/dist/utils/taskHierarchy.test.d.ts +1 -0
  433. package/dist/utils/taskHierarchy.test.js +38 -0
  434. package/dist/utils/taskParenting.d.ts +3 -0
  435. package/dist/utils/taskParenting.js +24 -0
  436. package/dist/utils/taskParenting.test.d.ts +1 -0
  437. package/dist/utils/taskParenting.test.js +21 -0
  438. package/dist/utils/taskRelations.d.ts +6 -0
  439. package/dist/utils/taskRelations.js +24 -0
  440. package/dist/utils/taskRelations.test.d.ts +1 -0
  441. package/dist/utils/taskRelations.test.js +34 -0
  442. package/dist/utils/taskUtils.d.ts +2 -0
  443. package/dist/utils/taskUtils.js +20 -0
  444. package/dist/utils/taskforceApiClient.d.ts +7 -0
  445. package/dist/utils/taskforceApiClient.js +42 -0
  446. package/dist/utils/workspaceIdentity.d.ts +8 -0
  447. package/dist/utils/workspaceIdentity.js +47 -0
  448. package/dist/utils/workspaceIdentity.test.d.ts +1 -0
  449. package/dist/utils/workspaceIdentity.test.js +32 -0
  450. package/dist/utils/workspaceLifecycle.d.ts +26 -0
  451. package/dist/utils/workspaceLifecycle.js +69 -0
  452. package/dist/utils/workspaceLifecycle.test.d.ts +1 -0
  453. package/dist/utils/workspaceLifecycle.test.js +64 -0
  454. package/package.json +133 -0
  455. package/scripts/check-compat-boundaries.js +48 -0
  456. package/scripts/check-env.ts +24 -0
  457. package/scripts/check-localization-strings.mjs +49 -0
  458. package/scripts/check-runtime-boundaries.mjs +44 -0
  459. package/scripts/debug-global.mjs +34 -0
  460. package/scripts/debug-server.mjs +36 -0
  461. package/scripts/export-sqlite-to-postgres.mjs +125 -0
  462. package/scripts/generate-env-example.ts +8 -0
  463. package/scripts/generate-workflow-templates.mjs +134 -0
  464. package/scripts/mcp-stdio-adapter.mjs +160 -0
  465. package/scripts/postinstall.js +60 -0
  466. package/scripts/release-version-lib.mjs +88 -0
  467. package/scripts/release-version.mjs +72 -0
  468. package/scripts/run-dev-proxy.sh +49 -0
  469. package/scripts/run-e2e-realtime.sh +16 -0
  470. package/scripts/run-smoke.sh +23 -0
  471. package/scripts/run-soak.sh +29 -0
  472. package/scripts/test-global.js +143 -0
  473. package/scripts/update-homebrew-formula.sh +38 -0
  474. package/src/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  475. package/src/resources/templates/environments/aider.yaml +14 -0
  476. package/src/resources/templates/environments/antigravity.yaml +10 -0
  477. package/src/resources/templates/environments/claude.yaml +10 -0
  478. package/src/resources/templates/environments/cline.yaml +10 -0
  479. package/src/resources/templates/environments/codex.yaml +13 -0
  480. package/src/resources/templates/environments/copilot.yaml +10 -0
  481. package/src/resources/templates/environments/cursor.yaml +9 -0
  482. package/src/resources/templates/environments/gemini.yaml +9 -0
  483. package/src/resources/templates/environments/windsurf.yaml +10 -0
  484. package/src/resources/templates/workflow-sources/README.md +9 -0
  485. package/src/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  486. package/src/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  487. package/src/resources/templates/workflows/collaborate.yaml +104 -0
  488. package/src/resources/templates/workflows/do.yaml +82 -0
  489. package/src/resources/templates/workflows/evaluate.yaml +111 -0
  490. package/src/resources/templates/workflows/execute.yaml +142 -0
  491. package/src/resources/templates/workflows/plan.yaml +112 -0
  492. package/src/resources/templates/workflows/review.yaml +109 -0
@@ -0,0 +1,2132 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
5
+ import { MemoryRouter } from 'react-router-dom';
6
+ import { TaskforceCore } from './TaskforceCore';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ const mockTasks = [
10
+ {
11
+ id: 'task-20260130-abc123',
12
+ title: 'Integration Task 1',
13
+ description: 'Testing core',
14
+ category: 'general',
15
+ type: 'feature',
16
+ priority: 2,
17
+ createdAt: new Date().toISOString(),
18
+ completed: false,
19
+ status: 'task'
20
+ }
21
+ ];
22
+ const mockConfig = {
23
+ storagePath: '.Taskforce',
24
+ shortcut: 'Alt+T',
25
+ projectName: 'Test Project',
26
+ setupState: {
27
+ globalSetupState: 'complete',
28
+ workspaceSetupState: 'complete',
29
+ workspaceId: 'default'
30
+ }
31
+ };
32
+ const mockCategories = {
33
+ categories: [{ value: 'general', label: 'General' }]
34
+ };
35
+ describe('TaskforceCore Integration', () => {
36
+ beforeEach(() => {
37
+ localStorage.clear();
38
+ vi.stubGlobal('fetch', vi.fn((url) => {
39
+ if (url.includes('/api/taskforce/config')) {
40
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockConfig) });
41
+ }
42
+ if (url.includes('/api/taskforce/categories')) {
43
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
44
+ }
45
+ if (url.includes('/api/taskforce/tasks')) {
46
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
47
+ }
48
+ if (url.includes('/api/taskforce/types')) {
49
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
50
+ }
51
+ if (url.includes('/api/taskforce/priorities')) {
52
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
53
+ }
54
+ if (url.includes('/api/taskforce/approaches')) {
55
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
56
+ }
57
+ if (url.includes('/api/taskforce/specialists')) {
58
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
59
+ }
60
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
61
+ }));
62
+ });
63
+ it('5.1 & 5.3 Renders main header and task list', async () => {
64
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
65
+ // Header check
66
+ expect(screen.getByText('TaskForce')).toBeInTheDocument();
67
+ // Wait for tasks to load and render
68
+ await waitFor(() => {
69
+ // Check project name
70
+ expect(screen.getByText('Test Project')).toBeInTheDocument();
71
+ expect(screen.getByText('1/1')).toBeInTheDocument();
72
+ });
73
+ });
74
+ it('5.2 Does not render the widget floating button', async () => {
75
+ const { container } = render(_jsx(TaskforceCore, {}));
76
+ expect(screen.queryByTitle('Open Taskforce')).not.toBeInTheDocument();
77
+ // Core should have its standalone main container
78
+ await waitFor(() => {
79
+ const root = container.querySelector('[class*="standaloneWrapper"]');
80
+ expect(root).toBeInTheDocument();
81
+ });
82
+ });
83
+ it('5.3 Calls onTaskCountChange with task count', async () => {
84
+ const onCountChange = vi.fn();
85
+ render(_jsx(TaskforceCore, { onTaskCountChange: onCountChange }));
86
+ await waitFor(() => {
87
+ expect(onCountChange).toHaveBeenCalledWith(1);
88
+ });
89
+ });
90
+ it('5.7 Deep Link: Opens specific task when short ID is provided', async () => {
91
+ render(_jsx(TaskforceCore, { initialTaskId: "abc123" }));
92
+ await waitFor(() => {
93
+ // Should be in Edit mode (Update button visible)
94
+ expect(screen.getByText('Update')).toBeInTheDocument();
95
+ // Form should have the title
96
+ expect(screen.getByDisplayValue('Integration Task 1')).toBeInTheDocument();
97
+ });
98
+ });
99
+ it('Navigation: Can switch to Settings', async () => {
100
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
101
+ await waitFor(() => {
102
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
103
+ });
104
+ });
105
+ it.skip('migrates legacy saved priority filters across active, archive, and kanban visibility', async () => {
106
+ const user = userEvent.setup();
107
+ const activeTasks = [
108
+ {
109
+ id: 'task-20260210-pri1',
110
+ title: 'Priority One Active',
111
+ description: null,
112
+ category: 'general',
113
+ type: 'feature',
114
+ priority: 1,
115
+ createdAt: new Date().toISOString(),
116
+ completed: false,
117
+ status: 'task'
118
+ },
119
+ {
120
+ id: 'task-20260210-pri2',
121
+ title: 'Priority Two Active',
122
+ description: null,
123
+ category: 'general',
124
+ type: 'feature',
125
+ priority: 2,
126
+ createdAt: new Date().toISOString(),
127
+ completed: false,
128
+ status: 'task'
129
+ }
130
+ ];
131
+ const archivedTasks = [
132
+ {
133
+ id: 'task-20260210-pri3-archived',
134
+ title: 'Priority Three Archived',
135
+ description: null,
136
+ category: 'general',
137
+ type: 'feature',
138
+ priority: 3,
139
+ createdAt: new Date().toISOString(),
140
+ completed: true,
141
+ status: 'done'
142
+ }
143
+ ];
144
+ const fetchMock = vi.fn((url) => {
145
+ if (url.includes('/api/taskforce/config')) {
146
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockConfig) });
147
+ }
148
+ if (url.includes('/api/taskforce/categories')) {
149
+ return Promise.resolve({
150
+ ok: true,
151
+ json: () => Promise.resolve({
152
+ categories: [{ value: 'general', label: 'General' }]
153
+ })
154
+ });
155
+ }
156
+ if (url.includes('/api/taskforce/tasks')) {
157
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: activeTasks }) });
158
+ }
159
+ if (url.includes('/api/taskforce/archive')) {
160
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: archivedTasks }) });
161
+ }
162
+ if (url.includes('/api/taskforce/priorities')) {
163
+ return Promise.resolve({
164
+ ok: true,
165
+ json: () => Promise.resolve({
166
+ priorities: [
167
+ { value: 1, label: 'P1' },
168
+ { value: 2, label: 'P2' },
169
+ { value: 3, label: 'P3' },
170
+ { value: 4, label: 'P4' }
171
+ ]
172
+ })
173
+ });
174
+ }
175
+ if (url.includes('/api/taskforce/types')) {
176
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [{ value: 'feature', label: 'Feature' }] }) });
177
+ }
178
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
179
+ return Promise.resolve({
180
+ ok: true,
181
+ json: () => Promise.resolve({
182
+ success: true,
183
+ state: {
184
+ hasInitedFilters: true,
185
+ filterPriorities: ['1', '3'],
186
+ filterCategories: ['general'],
187
+ filterTypes: ['feature'],
188
+ filterStatus: ['task', 'on-hold', 'in-progress', 'review', 'done', 'cancelled'],
189
+ groupBy: 'priority'
190
+ }
191
+ })
192
+ });
193
+ }
194
+ if (url.includes('/api/taskforce/ui-state')) {
195
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
196
+ }
197
+ if (url.includes('/api/taskforce/approaches')) {
198
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
199
+ }
200
+ if (url.includes('/api/taskforce/specialists')) {
201
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
202
+ }
203
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
204
+ });
205
+ vi.stubGlobal('fetch', fetchMock);
206
+ render(_jsx(TaskforceCore, { mode: "standalone" }));
207
+ await waitFor(() => {
208
+ expect(screen.getByText('Priority One Active')).toBeInTheDocument();
209
+ });
210
+ expect(screen.queryByText('Priority Two Active')).not.toBeInTheDocument();
211
+ await waitFor(() => {
212
+ expect(screen.getByTitle('Add task to P1')).toBeInTheDocument();
213
+ expect(screen.getByTitle('Add task to P3')).toBeInTheDocument();
214
+ });
215
+ expect(screen.queryByTitle('Add task to P2')).not.toBeInTheDocument();
216
+ await user.click(screen.getByTitle('Toggle Filters'));
217
+ await user.click(screen.getByRole('checkbox', { name: /include archive/i }));
218
+ await waitFor(() => {
219
+ expect(screen.getByText(/Priority Three Archived/)).toBeInTheDocument();
220
+ });
221
+ expect(fetchMock.mock.calls.some((call) => call[0] === '/api/taskforce/archive')).toBe(true);
222
+ });
223
+ it('shows full-page login when cloud auth is required and user is unauthenticated', async () => {
224
+ const fetchMock = vi.fn((url) => {
225
+ if (url.includes('/api/taskforce/auth/session')) {
226
+ return Promise.resolve({
227
+ ok: true,
228
+ json: () => Promise.resolve({
229
+ authenticated: false,
230
+ runtimeMode: 'cloud',
231
+ authRequiredForApi: true
232
+ })
233
+ });
234
+ }
235
+ if (url.includes('/api/taskforce/config')) {
236
+ return Promise.resolve({
237
+ ok: true,
238
+ json: () => Promise.resolve({
239
+ ...mockConfig,
240
+ runtimeMode: 'cloud',
241
+ authRequiredForApi: true,
242
+ setupState: {
243
+ globalSetupState: 'present',
244
+ workspaceSetupState: 'present',
245
+ runtimeMode: 'cloud',
246
+ workspaceId: 'default',
247
+ mode: 'core',
248
+ forceSetup: false,
249
+ forceGlobalSetup: false,
250
+ forceWorkspaceSetup: false
251
+ },
252
+ runtimeCapabilities: {
253
+ runtimeMode: 'cloud',
254
+ supportsCloudAuth: true,
255
+ supportsAdminManagedSetupOverrides: false,
256
+ supportsPackageScriptSetupOverrides: false
257
+ }
258
+ })
259
+ });
260
+ }
261
+ if (url.includes('/api/taskforce/tasks')) {
262
+ return Promise.resolve({ ok: false, status: 401, json: () => Promise.resolve({ error: 'Unauthorized' }) });
263
+ }
264
+ if (url.includes('/api/taskforce/workflow-templates')) {
265
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
266
+ }
267
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
268
+ });
269
+ vi.stubGlobal('fetch', fetchMock);
270
+ const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
271
+ await waitFor(() => {
272
+ expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
273
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
274
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
275
+ expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
276
+ expect(screen.queryByRole('button', { name: 'Close sign in' })).not.toBeInTheDocument();
277
+ });
278
+ expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
279
+ });
280
+ it('shows updated setup-bootstrap copy while startup checks are in progress', async () => {
281
+ const hangingConfig = new Promise(() => { });
282
+ const fetchMock = vi.fn((url) => {
283
+ if (url.includes('/api/taskforce/config')) {
284
+ return hangingConfig;
285
+ }
286
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
287
+ });
288
+ vi.stubGlobal('fetch', fetchMock);
289
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, {}) }));
290
+ await waitFor(() => {
291
+ expect(screen.getByText('Loading Taskforce')).toBeInTheDocument();
292
+ expect(screen.getByText(/Verifying session|Checking account, workspace access, and setup status/)).toBeInTheDocument();
293
+ });
294
+ expect(screen.queryByText('Loading Setup')).not.toBeInTheDocument();
295
+ });
296
+ it('keeps account menu in bootstrap-pending state until runtime config resolves, then shows signed-in identity immediately', async () => {
297
+ const user = userEvent.setup();
298
+ let resolveRuntimeConfig = null;
299
+ const fetchMock = vi.fn((url) => {
300
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
301
+ return new Promise((resolve) => {
302
+ resolveRuntimeConfig = resolve;
303
+ });
304
+ }
305
+ if (url.includes('https://cloud.example.com/api/taskforce/auth/session')) {
306
+ return Promise.resolve({
307
+ ok: true,
308
+ json: () => Promise.resolve({
309
+ authenticated: true,
310
+ runtimeMode: 'local',
311
+ authRequiredForApi: false,
312
+ email: 'member@example.com',
313
+ userId: 'user-member',
314
+ workspaceId: 'workspace-local-active',
315
+ betaAccess: true
316
+ })
317
+ });
318
+ }
319
+ if (url.includes('/api/taskforce/config')) {
320
+ return Promise.resolve({
321
+ ok: true,
322
+ json: () => Promise.resolve({
323
+ ...mockConfig,
324
+ runtimeMode: 'local',
325
+ authRequiredForApi: false,
326
+ workspaceId: 'workspace-local-active',
327
+ setupState: {
328
+ globalSetupState: 'present',
329
+ workspaceSetupState: 'present',
330
+ runtimeMode: 'local',
331
+ workspaceId: 'workspace-local-active',
332
+ mode: 'core',
333
+ forceSetup: false,
334
+ forceGlobalSetup: false,
335
+ forceWorkspaceSetup: false
336
+ }
337
+ })
338
+ });
339
+ }
340
+ if (url.includes('/api/taskforce/categories')) {
341
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
342
+ }
343
+ if (url.includes('/api/taskforce/tasks')) {
344
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
345
+ }
346
+ if (url.includes('/api/taskforce/types')) {
347
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
348
+ }
349
+ if (url.includes('/api/taskforce/priorities')) {
350
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
351
+ }
352
+ if (url.includes('/api/taskforce/approaches')) {
353
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
354
+ }
355
+ if (url.includes('/api/taskforce/specialists')) {
356
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
357
+ }
358
+ if (url.includes('/api/taskforce/workflow-templates')) {
359
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
360
+ }
361
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
362
+ });
363
+ vi.stubGlobal('fetch', fetchMock);
364
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
365
+ await waitFor(() => {
366
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
367
+ });
368
+ await user.click(screen.getByTitle(/Account/i));
369
+ expect(screen.getByText('Checking sign-in status...')).toBeInTheDocument();
370
+ expect(screen.queryByText('Sign In / Register')).not.toBeInTheDocument();
371
+ if (resolveRuntimeConfig) {
372
+ resolveRuntimeConfig({
373
+ ok: true,
374
+ json: () => Promise.resolve({
375
+ success: true,
376
+ config: {
377
+ baseUrl: 'https://cloud.example.com',
378
+ apiBaseUrl: 'https://cloud.example.com',
379
+ cloudAuthBaseUrl: 'https://cloud.example.com',
380
+ wsBaseUrl: null,
381
+ runtimeMode: 'local'
382
+ }
383
+ })
384
+ });
385
+ }
386
+ await waitFor(() => {
387
+ expect(screen.getByText(/Signed in as/i)).toBeInTheDocument();
388
+ expect(screen.getByText('member@example.com')).toBeInTheDocument();
389
+ });
390
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/workspaces'))).toBe(false);
391
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/session/workspace'))).toBe(false);
392
+ });
393
+ it('allows local runtime to open shared /login and register flow', async () => {
394
+ const fetchMock = vi.fn((url) => {
395
+ if (url.includes('/api/taskforce/auth/session')) {
396
+ return Promise.resolve({
397
+ ok: true,
398
+ json: () => Promise.resolve({
399
+ authenticated: false,
400
+ runtimeMode: 'local',
401
+ authRequiredForApi: false
402
+ })
403
+ });
404
+ }
405
+ if (url.includes('/api/taskforce/config')) {
406
+ return Promise.resolve({
407
+ ok: true,
408
+ json: () => Promise.resolve({
409
+ ...mockConfig,
410
+ runtimeMode: 'local',
411
+ authRequiredForApi: false,
412
+ setupState: {
413
+ globalSetupState: 'present',
414
+ workspaceSetupState: 'present',
415
+ runtimeMode: 'local',
416
+ workspaceId: 'workspace-local-active',
417
+ mode: 'core',
418
+ forceSetup: false,
419
+ forceGlobalSetup: false,
420
+ forceWorkspaceSetup: false
421
+ }
422
+ })
423
+ });
424
+ }
425
+ if (url.includes('/api/taskforce/workflow-templates')) {
426
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
427
+ }
428
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
429
+ });
430
+ vi.stubGlobal('fetch', fetchMock);
431
+ const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
432
+ await waitFor(() => {
433
+ expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
434
+ expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
435
+ expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
436
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
437
+ expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
438
+ expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
439
+ });
440
+ expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
441
+ });
442
+ it('allows returning to sign-in/register from forgot mode', async () => {
443
+ const user = userEvent.setup();
444
+ const fetchMock = vi.fn((url) => {
445
+ if (url.includes('/api/taskforce/auth/session')) {
446
+ return Promise.resolve({
447
+ ok: true,
448
+ json: () => Promise.resolve({
449
+ authenticated: false,
450
+ runtimeMode: 'local',
451
+ authRequiredForApi: false
452
+ })
453
+ });
454
+ }
455
+ if (url.includes('/api/taskforce/config')) {
456
+ return Promise.resolve({
457
+ ok: true,
458
+ json: () => Promise.resolve({
459
+ ...mockConfig,
460
+ runtimeMode: 'local',
461
+ authRequiredForApi: false,
462
+ setupState: {
463
+ globalSetupState: 'present',
464
+ workspaceSetupState: 'present',
465
+ runtimeMode: 'local',
466
+ workspaceId: 'workspace-local-active',
467
+ mode: 'core',
468
+ forceSetup: false,
469
+ forceGlobalSetup: false,
470
+ forceWorkspaceSetup: false
471
+ }
472
+ })
473
+ });
474
+ }
475
+ if (url.includes('/api/taskforce/workflow-templates')) {
476
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
477
+ }
478
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
479
+ });
480
+ vi.stubGlobal('fetch', fetchMock);
481
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
482
+ await waitFor(() => {
483
+ expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
484
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
485
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
486
+ expect(screen.queryByRole('button', { name: 'Accept Invite' })).not.toBeInTheDocument();
487
+ expect(screen.queryByRole('button', { name: /Resend Verification/ })).not.toBeInTheDocument();
488
+ });
489
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
490
+ await waitFor(() => {
491
+ expect(screen.getByText('Sign in with your account credentials.')).toBeInTheDocument();
492
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
493
+ });
494
+ });
495
+ it('shows global setup route when forced and persists selected mode', async () => {
496
+ const user = userEvent.setup();
497
+ let selectedMode = null;
498
+ const fetchMock = vi.fn((url, init) => {
499
+ if (url.includes('/api/taskforce/auth/session')) {
500
+ return Promise.resolve({
501
+ ok: true,
502
+ json: () => Promise.resolve({
503
+ authenticated: true,
504
+ runtimeMode: 'local',
505
+ authRequiredForApi: false
506
+ })
507
+ });
508
+ }
509
+ if (url.includes('/api/taskforce/config')) {
510
+ const mode = selectedMode;
511
+ return Promise.resolve({
512
+ ok: true,
513
+ json: () => Promise.resolve({
514
+ ...mockConfig,
515
+ runtimeMode: 'local',
516
+ authRequiredForApi: false,
517
+ setupState: {
518
+ globalSetupState: mode ? 'present' : 'missing',
519
+ workspaceSetupState: 'present',
520
+ runtimeMode: 'local',
521
+ workspaceId: 'default',
522
+ mode: mode || 'core',
523
+ forceSetup: false,
524
+ forceGlobalSetup: true,
525
+ forceWorkspaceSetup: false
526
+ },
527
+ runtimeCapabilities: {
528
+ runtimeMode: 'local',
529
+ supportsCloudAuth: false,
530
+ supportsAdminManagedSetupOverrides: false,
531
+ supportsPackageScriptSetupOverrides: true
532
+ }
533
+ })
534
+ });
535
+ }
536
+ if (url.includes('/api/taskforce/global-settings')) {
537
+ const body = JSON.parse(String(init?.body || '{}'));
538
+ selectedMode = body?.setup?.mode === 'operations' ? 'operations' : 'core';
539
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
540
+ }
541
+ if (url.includes('/api/taskforce/tasks')) {
542
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
543
+ }
544
+ if (url.includes('/api/taskforce/workflow-templates')) {
545
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
546
+ }
547
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
548
+ });
549
+ vi.stubGlobal('fetch', fetchMock);
550
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
551
+ await waitFor(() => {
552
+ expect(screen.getByText('Global Setup Required')).toBeInTheDocument();
553
+ });
554
+ await user.click(screen.getByRole('radio', { name: /operations mode/i }));
555
+ await user.click(screen.getByRole('button', { name: 'Save Global Setup' }));
556
+ await waitFor(() => {
557
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
558
+ });
559
+ expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/global-settings', expect.objectContaining({ method: 'POST' }));
560
+ });
561
+ it('shows workspace setup route when required and persists workspace profile', async () => {
562
+ const user = userEvent.setup();
563
+ let workspaceSaved = false;
564
+ const fetchMock = vi.fn((url, init) => {
565
+ if (url.includes('/api/taskforce/auth/session')) {
566
+ return Promise.resolve({
567
+ ok: true,
568
+ json: () => Promise.resolve({
569
+ authenticated: true,
570
+ runtimeMode: 'local',
571
+ authRequiredForApi: false
572
+ })
573
+ });
574
+ }
575
+ if (url.includes('/api/taskforce/config')) {
576
+ return Promise.resolve({
577
+ ok: true,
578
+ json: () => Promise.resolve({
579
+ ...mockConfig,
580
+ runtimeMode: 'local',
581
+ authRequiredForApi: false,
582
+ setupState: {
583
+ globalSetupState: 'present',
584
+ workspaceSetupState: workspaceSaved ? 'present' : 'missing',
585
+ runtimeMode: 'local',
586
+ workspaceId: 'default',
587
+ mode: 'core',
588
+ forceSetup: false,
589
+ forceGlobalSetup: false,
590
+ forceWorkspaceSetup: true,
591
+ workspace: workspaceSaved ? { id: 'default', slug: 'default', name: 'Delta Project', description: 'Workspace desc' } : null,
592
+ suggestedWorkspaceName: 'Suggested Project'
593
+ },
594
+ runtimeCapabilities: {
595
+ runtimeMode: 'local',
596
+ supportsCloudAuth: false,
597
+ supportsAdminManagedSetupOverrides: false,
598
+ supportsPackageScriptSetupOverrides: true
599
+ }
600
+ })
601
+ });
602
+ }
603
+ if (url.includes('/api/taskforce/workspace-profile')) {
604
+ const body = JSON.parse(String(init?.body || '{}'));
605
+ if (body?.name === 'Delta Project')
606
+ workspaceSaved = true;
607
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
608
+ }
609
+ if (url.includes('/api/taskforce/tasks')) {
610
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
611
+ }
612
+ if (url.includes('/api/taskforce/workflow-templates')) {
613
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
614
+ }
615
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
616
+ });
617
+ vi.stubGlobal('fetch', fetchMock);
618
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
619
+ await waitFor(() => {
620
+ expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
621
+ });
622
+ const nameInput = screen.getByPlaceholderText('Project name');
623
+ await user.clear(nameInput);
624
+ await user.type(nameInput, 'Delta Project');
625
+ await user.type(screen.getByPlaceholderText('Project description (optional)'), 'Workspace desc');
626
+ await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
627
+ await waitFor(() => {
628
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
629
+ });
630
+ expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
631
+ const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
632
+ if (!String(call[0]).includes('/api/taskforce/ui-state'))
633
+ return false;
634
+ const init = call[1];
635
+ if (String(init?.method || '').toUpperCase() !== 'POST')
636
+ return false;
637
+ const body = JSON.parse(String(init?.body || '{}'));
638
+ return body?.stateKey === 'workspace-sync';
639
+ });
640
+ const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
641
+ expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
642
+ expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(false);
643
+ });
644
+ it('recovers from transient local workspace-missing setup state after scope resolves on refresh', async () => {
645
+ let configCalls = 0;
646
+ const fetchMock = vi.fn((url) => {
647
+ if (url.includes('/api/taskforce/auth/session')) {
648
+ return Promise.resolve({
649
+ ok: true,
650
+ json: () => Promise.resolve({
651
+ authenticated: false,
652
+ runtimeMode: 'local',
653
+ authRequiredForApi: false
654
+ })
655
+ });
656
+ }
657
+ if (url.includes('/api/taskforce/config')) {
658
+ configCalls += 1;
659
+ const firstPass = configCalls === 1;
660
+ return Promise.resolve({
661
+ ok: true,
662
+ json: () => Promise.resolve({
663
+ ...mockConfig,
664
+ runtimeMode: 'local',
665
+ authRequiredForApi: false,
666
+ projectRoot: '/mock/local-project-refresh',
667
+ workspaceId: 'workspace-local-refresh',
668
+ setupState: {
669
+ globalSetupState: 'present',
670
+ workspaceSetupState: firstPass ? 'missing' : 'present',
671
+ runtimeMode: 'local',
672
+ workspaceId: 'workspace-local-refresh',
673
+ mode: 'core',
674
+ forceSetup: false,
675
+ forceGlobalSetup: false,
676
+ forceWorkspaceSetup: firstPass,
677
+ workspace: firstPass
678
+ ? null
679
+ : { id: 'workspace-local-refresh', slug: 'workspace-local-refresh', name: 'Local Refresh Project', description: null },
680
+ suggestedWorkspaceName: 'Local Refresh Project'
681
+ },
682
+ runtimeCapabilities: {
683
+ runtimeMode: 'local',
684
+ supportsCloudAuth: true,
685
+ supportsAdminManagedSetupOverrides: false,
686
+ supportsPackageScriptSetupOverrides: true
687
+ }
688
+ })
689
+ });
690
+ }
691
+ if (url.includes('/api/taskforce/categories')) {
692
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
693
+ }
694
+ if (url.includes('/api/taskforce/tasks')) {
695
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
696
+ }
697
+ if (url.includes('/api/taskforce/types')) {
698
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
699
+ }
700
+ if (url.includes('/api/taskforce/priorities')) {
701
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
702
+ }
703
+ if (url.includes('/api/taskforce/approaches')) {
704
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
705
+ }
706
+ if (url.includes('/api/taskforce/specialists')) {
707
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
708
+ }
709
+ if (url.includes('/api/taskforce/workflow-templates')) {
710
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
711
+ }
712
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
713
+ });
714
+ vi.stubGlobal('fetch', fetchMock);
715
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
716
+ await waitFor(() => {
717
+ expect(configCalls).toBeGreaterThan(1);
718
+ });
719
+ expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
720
+ expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
721
+ });
722
+ it('allows workspace setup to sync from selected cloud project into matching local workspace id', async () => {
723
+ const user = userEvent.setup();
724
+ let workspaceSaved = false;
725
+ const fetchMock = vi.fn((url, init) => {
726
+ if (url.includes('/api/taskforce/auth/session')) {
727
+ return Promise.resolve({
728
+ ok: true,
729
+ json: () => Promise.resolve({
730
+ authenticated: true,
731
+ runtimeMode: 'local',
732
+ authRequiredForApi: false
733
+ })
734
+ });
735
+ }
736
+ if (url.includes('/api/taskforce/config')) {
737
+ return Promise.resolve({
738
+ ok: true,
739
+ json: () => Promise.resolve({
740
+ ...mockConfig,
741
+ runtimeMode: 'local',
742
+ authRequiredForApi: false,
743
+ setupState: {
744
+ globalSetupState: 'present',
745
+ workspaceSetupState: workspaceSaved ? 'present' : 'missing',
746
+ runtimeMode: 'local',
747
+ workspaceId: workspaceSaved ? 'workspace-cloud-123' : 'default',
748
+ mode: 'core',
749
+ forceSetup: false,
750
+ forceGlobalSetup: false,
751
+ forceWorkspaceSetup: true,
752
+ workspace: workspaceSaved
753
+ ? { id: 'workspace-cloud-123', slug: 'workspace-cloud-123', name: 'Cloud Project 123', description: 'Cloud desc' }
754
+ : null,
755
+ suggestedWorkspaceName: 'Suggested Project'
756
+ },
757
+ runtimeCapabilities: {
758
+ runtimeMode: 'local',
759
+ supportsCloudAuth: true,
760
+ supportsAdminManagedSetupOverrides: false,
761
+ supportsPackageScriptSetupOverrides: true
762
+ }
763
+ })
764
+ });
765
+ }
766
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
767
+ return Promise.resolve({
768
+ ok: true,
769
+ json: () => Promise.resolve({
770
+ success: true,
771
+ workspaces: [
772
+ {
773
+ id: 'workspace-cloud-123',
774
+ name: 'Cloud Project 123',
775
+ description: 'Cloud desc'
776
+ }
777
+ ]
778
+ })
779
+ });
780
+ }
781
+ if (url.includes('/api/taskforce/workspace-profile')) {
782
+ const body = JSON.parse(String(init?.body || '{}'));
783
+ if (body?.workspaceId === 'workspace-cloud-123')
784
+ workspaceSaved = true;
785
+ return Promise.resolve({
786
+ ok: true,
787
+ json: () => Promise.resolve({
788
+ success: true,
789
+ workspace: {
790
+ id: 'workspace-cloud-123'
791
+ }
792
+ })
793
+ });
794
+ }
795
+ if (url.includes('/api/taskforce/session/workspace')) {
796
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
797
+ }
798
+ if (url.includes('/api/taskforce/ui-state')) {
799
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
800
+ }
801
+ if (url.includes('/api/taskforce/tasks')) {
802
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
803
+ }
804
+ if (url.includes('/api/taskforce/workflow-templates')) {
805
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
806
+ }
807
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
808
+ });
809
+ vi.stubGlobal('fetch', fetchMock);
810
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
811
+ await waitFor(() => {
812
+ expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
813
+ });
814
+ await user.click(screen.getByRole('radio', { name: /sync existing cloud project/i }));
815
+ await waitFor(() => {
816
+ expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
817
+ });
818
+ await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
819
+ await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
820
+ await waitFor(() => {
821
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
822
+ });
823
+ const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
824
+ const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
825
+ expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
826
+ const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
827
+ if (!String(call[0]).includes('/api/taskforce/ui-state'))
828
+ return false;
829
+ const init = call[1];
830
+ if (String(init?.method || '').toUpperCase() !== 'POST')
831
+ return false;
832
+ const body = JSON.parse(String(init?.body || '{}'));
833
+ return body?.stateKey === 'workspace-sync';
834
+ });
835
+ const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
836
+ expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
837
+ expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
838
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
839
+ });
840
+ it('enables workspace cloud sync from local setup when signed in and checkbox is selected', async () => {
841
+ const user = userEvent.setup();
842
+ let workspaceSaved = false;
843
+ const fetchMock = vi.fn((url, init) => {
844
+ if (url.includes('/api/taskforce/auth/session')) {
845
+ return Promise.resolve({
846
+ ok: true,
847
+ json: () => Promise.resolve({
848
+ authenticated: true,
849
+ runtimeMode: 'local',
850
+ authRequiredForApi: false
851
+ })
852
+ });
853
+ }
854
+ if (url.includes('/api/taskforce/config')) {
855
+ return Promise.resolve({
856
+ ok: true,
857
+ json: () => Promise.resolve({
858
+ ...mockConfig,
859
+ runtimeMode: 'local',
860
+ authRequiredForApi: false,
861
+ setupState: {
862
+ globalSetupState: 'present',
863
+ workspaceSetupState: workspaceSaved ? 'present' : 'missing',
864
+ runtimeMode: 'local',
865
+ workspaceId: workspaceSaved ? 'workspace-local-setup-1' : 'default',
866
+ mode: 'core',
867
+ forceSetup: false,
868
+ forceGlobalSetup: false,
869
+ forceWorkspaceSetup: true,
870
+ workspace: workspaceSaved
871
+ ? { id: 'workspace-local-setup-1', slug: 'workspace-local-setup-1', name: 'Local Project', description: 'Local desc' }
872
+ : null,
873
+ suggestedWorkspaceName: 'Suggested Project'
874
+ },
875
+ runtimeCapabilities: {
876
+ runtimeMode: 'local',
877
+ supportsCloudAuth: true,
878
+ supportsAdminManagedSetupOverrides: false,
879
+ supportsPackageScriptSetupOverrides: true
880
+ }
881
+ })
882
+ });
883
+ }
884
+ if (url.includes('/api/taskforce/workspace-profile')) {
885
+ workspaceSaved = true;
886
+ return Promise.resolve({
887
+ ok: true,
888
+ json: () => Promise.resolve({
889
+ success: true,
890
+ workspace: {
891
+ id: 'workspace-local-setup-1'
892
+ }
893
+ })
894
+ });
895
+ }
896
+ if (url.includes('/api/taskforce/ui-state')) {
897
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
898
+ }
899
+ if (url.includes('/api/taskforce/tasks')) {
900
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
901
+ }
902
+ if (url.includes('/api/taskforce/workflow-templates')) {
903
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
904
+ }
905
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
906
+ });
907
+ vi.stubGlobal('fetch', fetchMock);
908
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
909
+ await waitFor(() => {
910
+ expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
911
+ });
912
+ await waitFor(() => {
913
+ expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
914
+ });
915
+ await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
916
+ const nameInput = screen.getByPlaceholderText('Project name');
917
+ await user.clear(nameInput);
918
+ await user.type(nameInput, 'Local Project');
919
+ await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
920
+ await waitFor(() => {
921
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
922
+ });
923
+ const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
924
+ if (!String(call[0]).includes('/api/taskforce/ui-state'))
925
+ return false;
926
+ const req = call[1];
927
+ if (String(req?.method || '').toUpperCase() !== 'POST')
928
+ return false;
929
+ const body = JSON.parse(String(req?.body || '{}'));
930
+ return body?.stateKey === 'workspace-sync';
931
+ });
932
+ const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
933
+ expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
934
+ expect(workspaceSyncStateBody.patch?.sourceOfTruth).toBe('local');
935
+ // I6: onboarding now starts as false; the orchestrator marks it true after first pull.
936
+ expect(workspaceSyncStateBody.patch?.onboardingCompleted).toBe(false);
937
+ });
938
+ it('completes local first-run setup, survives refresh, and provisions cloud workspace on sync', async () => {
939
+ const user = userEvent.setup();
940
+ let workspaceSaved = false;
941
+ let workspaceSyncState = {
942
+ cloudSyncEnabled: false,
943
+ sourceOfTruth: null,
944
+ onboardingCompleted: false
945
+ };
946
+ let cloudWorkspaceCreated = false;
947
+ const fetchMock = vi.fn((url, init) => {
948
+ if (url.includes('/api/taskforce/auth/session')) {
949
+ return Promise.resolve({
950
+ ok: true,
951
+ json: () => Promise.resolve({
952
+ authenticated: true,
953
+ runtimeMode: 'local',
954
+ authRequiredForApi: false,
955
+ workspaceId: workspaceSaved ? 'workspace-local-flow-1' : 'default'
956
+ })
957
+ });
958
+ }
959
+ if (url.includes('/api/taskforce/config')) {
960
+ return Promise.resolve({
961
+ ok: true,
962
+ json: () => Promise.resolve({
963
+ ...mockConfig,
964
+ runtimeMode: 'local',
965
+ authRequiredForApi: false,
966
+ workspaceId: workspaceSaved ? 'workspace-local-flow-1' : 'default',
967
+ setupState: {
968
+ globalSetupState: 'present',
969
+ workspaceSetupState: workspaceSaved ? 'present' : 'missing',
970
+ runtimeMode: 'local',
971
+ workspaceId: workspaceSaved ? 'workspace-local-flow-1' : 'default',
972
+ mode: 'core',
973
+ forceSetup: false,
974
+ forceGlobalSetup: false,
975
+ forceWorkspaceSetup: !workspaceSaved,
976
+ workspace: workspaceSaved
977
+ ? { id: 'workspace-local-flow-1', slug: 'workspace-local-flow-1', name: 'Local Flow Project', description: 'Flow desc' }
978
+ : null,
979
+ suggestedWorkspaceName: 'Suggested Project'
980
+ },
981
+ runtimeCapabilities: {
982
+ runtimeMode: 'local',
983
+ supportsCloudAuth: true,
984
+ supportsAdminManagedSetupOverrides: false,
985
+ supportsPackageScriptSetupOverrides: true
986
+ }
987
+ })
988
+ });
989
+ }
990
+ if (url.includes('/api/taskforce/workspace-profile')) {
991
+ workspaceSaved = true;
992
+ return Promise.resolve({
993
+ ok: true,
994
+ json: () => Promise.resolve({
995
+ success: true,
996
+ workspace: { id: 'workspace-local-flow-1' }
997
+ })
998
+ });
999
+ }
1000
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1001
+ return Promise.resolve({
1002
+ ok: true,
1003
+ json: () => Promise.resolve({
1004
+ success: true,
1005
+ state: workspaceSyncState
1006
+ })
1007
+ });
1008
+ }
1009
+ if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
1010
+ const body = JSON.parse(String(init?.body || '{}'));
1011
+ if (body?.stateKey === 'workspace-sync' && body?.patch && typeof body.patch === 'object') {
1012
+ // Merge patch fields only (mirrors server behaviour) so partial patches
1013
+ // like { onboardingCompleted: true } don't reset cloudSyncEnabled to false.
1014
+ if (body.patch.cloudSyncEnabled !== undefined)
1015
+ workspaceSyncState.cloudSyncEnabled = Boolean(body.patch.cloudSyncEnabled);
1016
+ if (body.patch.sourceOfTruth !== undefined)
1017
+ workspaceSyncState.sourceOfTruth = (body.patch.sourceOfTruth === 'cloud' || body.patch.sourceOfTruth === 'local') ? body.patch.sourceOfTruth : null;
1018
+ if (body.patch.onboardingCompleted !== undefined)
1019
+ workspaceSyncState.onboardingCompleted = Boolean(body.patch.onboardingCompleted);
1020
+ }
1021
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1022
+ }
1023
+ if (url.includes('/api/taskforce/sync/workspace/handshake')) {
1024
+ if (!cloudWorkspaceCreated) {
1025
+ return Promise.resolve({
1026
+ ok: false,
1027
+ status: 409,
1028
+ json: () => Promise.resolve({
1029
+ success: false,
1030
+ code: 'WORKSPACE_ID_MISMATCH',
1031
+ error: 'Workspace mismatch'
1032
+ })
1033
+ });
1034
+ }
1035
+ return Promise.resolve({
1036
+ ok: true,
1037
+ json: () => Promise.resolve({
1038
+ success: true,
1039
+ localWorkspaceId: 'workspace-local-flow-1',
1040
+ cloudWorkspaceId: 'workspace-local-flow-1'
1041
+ })
1042
+ });
1043
+ }
1044
+ if (url.includes('/api/taskforce/sync/workspace/provision')) {
1045
+ cloudWorkspaceCreated = true;
1046
+ return Promise.resolve({
1047
+ ok: true,
1048
+ status: 201,
1049
+ json: () => Promise.resolve({
1050
+ success: true,
1051
+ existing: false,
1052
+ workspace: { id: 'workspace-local-flow-1', name: 'Local Flow Project' }
1053
+ })
1054
+ });
1055
+ }
1056
+ if (url.includes('/api/taskforce/sync/workspace/pull')) {
1057
+ return Promise.resolve({
1058
+ ok: true,
1059
+ json: () => Promise.resolve({
1060
+ success: true,
1061
+ workspaceId: 'workspace-local-flow-1',
1062
+ changes: [],
1063
+ nextCursor: '',
1064
+ hasMore: false
1065
+ })
1066
+ });
1067
+ }
1068
+ if (url.includes('/api/taskforce/sync/workspace/push')) {
1069
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1070
+ }
1071
+ if (url.includes('/api/taskforce/categories'))
1072
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1073
+ if (url.includes('/api/taskforce/tasks'))
1074
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
1075
+ if (url.includes('/api/taskforce/archive'))
1076
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1077
+ if (url.includes('/api/taskforce/types'))
1078
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1079
+ if (url.includes('/api/taskforce/priorities'))
1080
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1081
+ if (url.includes('/api/taskforce/approaches'))
1082
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1083
+ if (url.includes('/api/taskforce/specialists'))
1084
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1085
+ if (url.includes('/api/taskforce/workflow-templates'))
1086
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1087
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1088
+ });
1089
+ vi.stubGlobal('fetch', fetchMock);
1090
+ const initialRender = render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
1091
+ await waitFor(() => {
1092
+ expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
1093
+ });
1094
+ await waitFor(() => {
1095
+ expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
1096
+ });
1097
+ await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
1098
+ const nameInput = screen.getByPlaceholderText('Project name');
1099
+ await user.clear(nameInput);
1100
+ await user.type(nameInput, 'Local Flow Project');
1101
+ await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
1102
+ await waitFor(() => {
1103
+ expect(workspaceSaved).toBe(true);
1104
+ });
1105
+ initialRender.unmount();
1106
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
1107
+ await waitFor(() => {
1108
+ expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
1109
+ expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
1110
+ });
1111
+ expect(cloudWorkspaceCreated).toBe(false);
1112
+ });
1113
+ it('shows local runtime Sign In / Register action and routes to shared login', async () => {
1114
+ const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
1115
+ const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
1116
+ expect(standaloneLayoutSource).toContain('Sign In / Register');
1117
+ expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
1118
+ expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
1119
+ });
1120
+ it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
1121
+ const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
1122
+ const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
1123
+ const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
1124
+ const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
1125
+ expect(standaloneLayoutSource).not.toContain('showCloudSignInModal');
1126
+ expect(standaloneLayoutSource).not.toContain('cloudSignInEmail');
1127
+ expect(useTaskforceSource).toContain("resolveCloudAuthUrl('/api/taskforce/auth/login')");
1128
+ expect(useTaskforceSource).toContain("return { success: false, error: 'Sign in failed.'");
1129
+ });
1130
+ it('does not promote auth state from generic task fetch success', async () => {
1131
+ const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
1132
+ const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
1133
+ // Auth state should be derived from session/login flows, not inferred from /tasks success.
1134
+ expect(useTaskforceSource).not.toContain('setIsAuthenticated(true);');
1135
+ });
1136
+ it('uses production cloud fallback for local runtime auth/sync endpoints', async () => {
1137
+ const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
1138
+ const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
1139
+ const appMainPath = path.join(process.cwd(), 'apps/app/main.tsx');
1140
+ const appMainSource = fs.readFileSync(appMainPath, 'utf8');
1141
+ expect(useTaskforceSource).toContain("return 'https://app.taskforcehq.ai';");
1142
+ expect(appMainSource).toContain('const envBase = String(import.meta.env.VITE_TASKFORCE_BASE_URL || \'\').trim();');
1143
+ expect(appMainSource).toContain('return undefined;');
1144
+ });
1145
+ it('keeps local-only runtime behavior when signed out (no workspace cloud pull calls)', async () => {
1146
+ const fetchMock = vi.fn((url) => {
1147
+ if (url.includes('/api/taskforce/auth/session')) {
1148
+ return Promise.resolve({
1149
+ ok: true,
1150
+ json: () => Promise.resolve({
1151
+ authenticated: false,
1152
+ runtimeMode: 'local',
1153
+ authRequiredForApi: false
1154
+ })
1155
+ });
1156
+ }
1157
+ if (url.includes('/api/taskforce/config')) {
1158
+ return Promise.resolve({
1159
+ ok: true,
1160
+ json: () => Promise.resolve({
1161
+ ...mockConfig,
1162
+ runtimeMode: 'local',
1163
+ authRequiredForApi: false,
1164
+ workspaceId: 'workspace-local-active'
1165
+ })
1166
+ });
1167
+ }
1168
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1169
+ return Promise.resolve({
1170
+ ok: true,
1171
+ json: () => Promise.resolve({
1172
+ success: true,
1173
+ state: {
1174
+ cloudSyncEnabled: true,
1175
+ sourceOfTruth: 'cloud',
1176
+ onboardingCompleted: true,
1177
+ pullCursor: ''
1178
+ }
1179
+ })
1180
+ });
1181
+ }
1182
+ if (url.includes('/api/taskforce/categories'))
1183
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1184
+ if (url.includes('/api/taskforce/tasks'))
1185
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
1186
+ if (url.includes('/api/taskforce/archive'))
1187
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1188
+ if (url.includes('/api/taskforce/types'))
1189
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1190
+ if (url.includes('/api/taskforce/priorities'))
1191
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1192
+ if (url.includes('/api/taskforce/approaches'))
1193
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1194
+ if (url.includes('/api/taskforce/specialists'))
1195
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1196
+ if (url.includes('/api/taskforce/workflow-templates'))
1197
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1198
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1199
+ });
1200
+ vi.stubGlobal('fetch', fetchMock);
1201
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1202
+ await waitFor(() => {
1203
+ expect(screen.getByTitle(/Account/)).toBeInTheDocument();
1204
+ });
1205
+ await waitFor(() => {
1206
+ const uiStateCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/api/taskforce/ui-state?key=workspace-sync'));
1207
+ expect(uiStateCalls.length).toBeGreaterThan(0);
1208
+ });
1209
+ const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
1210
+ expect(pullCalls.length).toBe(0);
1211
+ });
1212
+ it('does not pull from cloud when workspace sync toggle is disabled', async () => {
1213
+ const fetchMock = vi.fn((url) => {
1214
+ if (url.includes('/api/taskforce/auth/session')) {
1215
+ return Promise.resolve({
1216
+ ok: true,
1217
+ json: () => Promise.resolve({
1218
+ authenticated: true,
1219
+ runtimeMode: 'local',
1220
+ authRequiredForApi: false
1221
+ })
1222
+ });
1223
+ }
1224
+ if (url.includes('/api/taskforce/config')) {
1225
+ return Promise.resolve({
1226
+ ok: true,
1227
+ json: () => Promise.resolve({
1228
+ ...mockConfig,
1229
+ runtimeMode: 'local',
1230
+ authRequiredForApi: false,
1231
+ workspaceId: 'workspace-local-active'
1232
+ })
1233
+ });
1234
+ }
1235
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1236
+ return Promise.resolve({
1237
+ ok: true,
1238
+ json: () => Promise.resolve({
1239
+ success: true,
1240
+ state: {
1241
+ cloudSyncEnabled: false,
1242
+ sourceOfTruth: 'cloud',
1243
+ onboardingCompleted: true,
1244
+ pullCursor: ''
1245
+ }
1246
+ })
1247
+ });
1248
+ }
1249
+ if (url.includes('/api/taskforce/categories'))
1250
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1251
+ if (url.includes('/api/taskforce/tasks'))
1252
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
1253
+ if (url.includes('/api/taskforce/archive'))
1254
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1255
+ if (url.includes('/api/taskforce/types'))
1256
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1257
+ if (url.includes('/api/taskforce/priorities'))
1258
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1259
+ if (url.includes('/api/taskforce/approaches'))
1260
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1261
+ if (url.includes('/api/taskforce/specialists'))
1262
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1263
+ if (url.includes('/api/taskforce/workflow-templates'))
1264
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1265
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1266
+ });
1267
+ vi.stubGlobal('fetch', fetchMock);
1268
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1269
+ await waitFor(() => {
1270
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1271
+ });
1272
+ await waitFor(() => {
1273
+ const uiStateCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/api/taskforce/ui-state?key=workspace-sync'));
1274
+ expect(uiStateCalls.length).toBeGreaterThan(0);
1275
+ });
1276
+ const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
1277
+ expect(pullCalls.length).toBe(0);
1278
+ });
1279
+ it('does not repeatedly reload workspace-sync ui-state on equivalent rerenders', async () => {
1280
+ let workspaceSyncReads = 0;
1281
+ const fetchMock = vi.fn((url) => {
1282
+ if (url.includes('/api/taskforce/auth/session')) {
1283
+ return Promise.resolve({
1284
+ ok: true,
1285
+ json: () => Promise.resolve({
1286
+ authenticated: true,
1287
+ runtimeMode: 'local',
1288
+ authRequiredForApi: false
1289
+ })
1290
+ });
1291
+ }
1292
+ if (url.includes('/api/taskforce/config')) {
1293
+ return Promise.resolve({
1294
+ ok: true,
1295
+ json: () => Promise.resolve({
1296
+ ...mockConfig,
1297
+ runtimeMode: 'local',
1298
+ authRequiredForApi: false,
1299
+ workspaceId: 'workspace-local-active'
1300
+ })
1301
+ });
1302
+ }
1303
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1304
+ workspaceSyncReads += 1;
1305
+ return Promise.resolve({
1306
+ ok: true,
1307
+ json: () => Promise.resolve({
1308
+ success: true,
1309
+ state: {
1310
+ cloudSyncEnabled: true,
1311
+ sourceOfTruth: 'local',
1312
+ onboardingCompleted: true,
1313
+ pullCursor: ''
1314
+ }
1315
+ })
1316
+ });
1317
+ }
1318
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
1319
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
1320
+ }
1321
+ if (url.includes('/api/taskforce/categories'))
1322
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1323
+ if (url.includes('/api/taskforce/tasks'))
1324
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
1325
+ if (url.includes('/api/taskforce/archive'))
1326
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1327
+ if (url.includes('/api/taskforce/types'))
1328
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1329
+ if (url.includes('/api/taskforce/priorities'))
1330
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1331
+ if (url.includes('/api/taskforce/approaches'))
1332
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1333
+ if (url.includes('/api/taskforce/specialists'))
1334
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1335
+ if (url.includes('/api/taskforce/workflow-templates'))
1336
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1337
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1338
+ });
1339
+ vi.stubGlobal('fetch', fetchMock);
1340
+ const { rerender } = render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1341
+ await waitFor(() => {
1342
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1343
+ });
1344
+ await waitFor(() => {
1345
+ expect(workspaceSyncReads).toBeGreaterThan(0);
1346
+ });
1347
+ const baselineReads = workspaceSyncReads;
1348
+ rerender(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1349
+ await new Promise((resolve) => window.setTimeout(resolve, 150));
1350
+ expect(workspaceSyncReads).toBe(baselineReads);
1351
+ });
1352
+ it('sync status modal displays runtime mode and sync source of truth separately', async () => {
1353
+ const user = userEvent.setup();
1354
+ const fetchMock = vi.fn((url) => {
1355
+ if (url.includes('/api/taskforce/auth/session')) {
1356
+ return Promise.resolve({
1357
+ ok: true,
1358
+ json: () => Promise.resolve({
1359
+ authenticated: true,
1360
+ runtimeMode: 'local',
1361
+ authRequiredForApi: false
1362
+ })
1363
+ });
1364
+ }
1365
+ if (url.includes('/api/taskforce/config')) {
1366
+ return Promise.resolve({
1367
+ ok: true,
1368
+ json: () => Promise.resolve({
1369
+ ...mockConfig,
1370
+ runtimeMode: 'local',
1371
+ authRequiredForApi: false,
1372
+ workspaceId: 'workspace-local-active'
1373
+ })
1374
+ });
1375
+ }
1376
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1377
+ return Promise.resolve({
1378
+ ok: true,
1379
+ json: () => Promise.resolve({
1380
+ success: true,
1381
+ state: {
1382
+ cloudSyncEnabled: true,
1383
+ sourceOfTruth: 'cloud',
1384
+ onboardingCompleted: true,
1385
+ pullCursor: ''
1386
+ }
1387
+ })
1388
+ });
1389
+ }
1390
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
1391
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
1392
+ }
1393
+ if (url.includes('/api/taskforce/categories'))
1394
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1395
+ if (url.includes('/api/taskforce/tasks'))
1396
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
1397
+ if (url.includes('/api/taskforce/archive'))
1398
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1399
+ if (url.includes('/api/taskforce/types'))
1400
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1401
+ if (url.includes('/api/taskforce/priorities'))
1402
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1403
+ if (url.includes('/api/taskforce/approaches'))
1404
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1405
+ if (url.includes('/api/taskforce/specialists'))
1406
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1407
+ if (url.includes('/api/taskforce/workflow-templates'))
1408
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1409
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1410
+ });
1411
+ vi.stubGlobal('fetch', fetchMock);
1412
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1413
+ await waitFor(() => {
1414
+ expect(screen.getByTitle(/Cloud sync status:/i)).toBeInTheDocument();
1415
+ });
1416
+ await user.click(screen.getByTitle(/Cloud sync status:/i));
1417
+ await waitFor(() => {
1418
+ expect(screen.getByText('Runtime mode')).toBeInTheDocument();
1419
+ expect(screen.getByText('local')).toBeInTheDocument();
1420
+ expect(screen.getByText('Sync source of truth')).toBeInTheDocument();
1421
+ expect(screen.getByText('cloud')).toBeInTheDocument();
1422
+ });
1423
+ });
1424
+ it('uses apply-local route with workspace header when cloud pull merges into local workspace', async () => {
1425
+ const cloudSyncApiPath = path.join(process.cwd(), 'src/sync/cloudSyncApi.ts');
1426
+ const cloudSyncApiSource = fs.readFileSync(cloudSyncApiPath, 'utf8');
1427
+ expect(cloudSyncApiSource).toContain("'/api/taskforce/sync/workspace/apply-local'");
1428
+ expect(cloudSyncApiSource).toContain("'x-taskforce-workspace-id': workspaceId");
1429
+ expect(cloudSyncApiSource).toContain("if (pathname === '/taskforce' || pathname.startsWith('/taskforce/'))");
1430
+ expect(cloudSyncApiSource).toContain("return '/taskforce/api/taskforce/sync/workspace/apply-local';");
1431
+ });
1432
+ it('pulls cloud tasks into the active non-default local workspace', async () => {
1433
+ const cloudSyncApiPath = path.join(process.cwd(), 'src/sync/cloudSyncApi.ts');
1434
+ const cloudSyncApiSource = fs.readFileSync(cloudSyncApiPath, 'utf8');
1435
+ const syncServicePath = path.join(process.cwd(), 'src/sync/syncService.ts');
1436
+ const syncServiceSource = fs.readFileSync(syncServicePath, 'utf8');
1437
+ expect(cloudSyncApiSource).toContain("new URLSearchParams({ limit: String(input.limit), workspaceId: input.workspaceId })");
1438
+ expect(cloudSyncApiSource).toContain("resolveCloudAuthUrl('/api/taskforce/sync/workspace/pull')");
1439
+ expect(cloudSyncApiSource).toContain("const applyPayload = JSON.stringify({ workspaceId, changes });");
1440
+ expect(syncServiceSource).toContain('const applyResult = await applyLocalWorkspaceChanges(input.workspaceId, changes);');
1441
+ });
1442
+ it('does not push local changes before first cloud hydration when source is cloud', async () => {
1443
+ const orchestratorPath = path.join(process.cwd(), 'src/hooks/useSyncOrchestrator.ts');
1444
+ const orchestratorSource = fs.readFileSync(orchestratorPath, 'utf8');
1445
+ expect(orchestratorSource).toContain('push_blocked_waiting_hydration_force_pull');
1446
+ expect(orchestratorSource).toContain('onboardingCompletedRef.current');
1447
+ expect(orchestratorSource).toContain('If a local mutation happened before initial cloud hydration finished, flush it now.');
1448
+ expect(orchestratorSource).toContain('persistWorkspaceSyncPatch({ onboardingCompleted: true })');
1449
+ });
1450
+ it('keeps local workspace context authoritative over cloud auth session workspace id', async () => {
1451
+ const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
1452
+ const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
1453
+ expect(useTaskforceSource).toContain("const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;");
1454
+ expect(useTaskforceSource).toContain('Local runtime resolves workspace identity from local DB via /api/taskforce/config.');
1455
+ expect(useTaskforceSource).toContain('Do not override it from cloud auth session payloads.');
1456
+ });
1457
+ it.skip('blocks local cloud-sync pull/apply when workspaceId remains default', async () => {
1458
+ const fetchMock = vi.fn((url, init) => {
1459
+ if (url.includes('/api/taskforce/auth/session')) {
1460
+ return Promise.resolve({
1461
+ ok: true,
1462
+ json: () => Promise.resolve({
1463
+ authenticated: true,
1464
+ runtimeMode: 'local',
1465
+ authRequiredForApi: false,
1466
+ workspaceId: 'default'
1467
+ })
1468
+ });
1469
+ }
1470
+ if (url.includes('/api/taskforce/config')) {
1471
+ return Promise.resolve({
1472
+ ok: true,
1473
+ json: () => Promise.resolve({
1474
+ ...mockConfig,
1475
+ runtimeMode: 'local',
1476
+ authRequiredForApi: false,
1477
+ workspaceId: 'default'
1478
+ })
1479
+ });
1480
+ }
1481
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1482
+ return Promise.resolve({
1483
+ ok: true,
1484
+ json: () => Promise.resolve({
1485
+ success: true,
1486
+ state: {
1487
+ cloudSyncEnabled: true,
1488
+ sourceOfTruth: 'cloud',
1489
+ onboardingCompleted: true,
1490
+ pullCursor: ''
1491
+ }
1492
+ })
1493
+ });
1494
+ }
1495
+ if (url.includes('/api/taskforce/tasks'))
1496
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
1497
+ if (url.includes('/api/taskforce/archive'))
1498
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1499
+ if (url.includes('/api/taskforce/categories'))
1500
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1501
+ if (url.includes('/api/taskforce/types'))
1502
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1503
+ if (url.includes('/api/taskforce/priorities'))
1504
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1505
+ if (url.includes('/api/taskforce/approaches'))
1506
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1507
+ if (url.includes('/api/taskforce/specialists'))
1508
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1509
+ if (url.includes('/api/taskforce/workflow-templates'))
1510
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1511
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
1512
+ return Promise.resolve({
1513
+ ok: true,
1514
+ json: () => Promise.resolve({
1515
+ success: true,
1516
+ changes: [{ op: 'upsert', archived: false, task: { id: 'task-cloud-generated-id' } }],
1517
+ nextCursor: 'cursor-default-block',
1518
+ hasMore: false
1519
+ })
1520
+ });
1521
+ }
1522
+ if (url === '/api/taskforce/sync/workspace/apply-local') {
1523
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1524
+ }
1525
+ if (url.includes('/api/taskforce/ui-state'))
1526
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1527
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1528
+ });
1529
+ vi.stubGlobal('fetch', fetchMock);
1530
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1531
+ await waitFor(() => {
1532
+ expect(screen.getByTitle(/Cloud sync status:/i)).toBeInTheDocument();
1533
+ });
1534
+ const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
1535
+ expect(pullCalls.length).toBe(0);
1536
+ const applyCalls = fetchMock.mock.calls.filter((call) => call[0] === '/api/taskforce/sync/workspace/apply-local');
1537
+ expect(applyCalls.length).toBe(0);
1538
+ });
1539
+ it.skip('removes cloud-deleted tasks from UI immediately even when local refresh request fails', async () => {
1540
+ const localTask = {
1541
+ id: 'task-delete-immediate-1',
1542
+ title: 'Delete me immediately',
1543
+ description: null,
1544
+ category: 'general',
1545
+ type: 'feature',
1546
+ priority: 2,
1547
+ createdAt: new Date().toISOString(),
1548
+ status: 'task'
1549
+ };
1550
+ let tasksRequestCount = 0;
1551
+ let releasePull;
1552
+ const pullGate = new Promise((resolve) => {
1553
+ releasePull = resolve;
1554
+ });
1555
+ const fetchMock = vi.fn((url) => {
1556
+ if (url.includes('/api/taskforce/auth/session')) {
1557
+ return Promise.resolve({
1558
+ ok: true,
1559
+ json: () => Promise.resolve({
1560
+ authenticated: true,
1561
+ runtimeMode: 'local',
1562
+ authRequiredForApi: false
1563
+ })
1564
+ });
1565
+ }
1566
+ if (url.includes('/api/taskforce/config')) {
1567
+ return Promise.resolve({
1568
+ ok: true,
1569
+ json: () => Promise.resolve({
1570
+ ...mockConfig,
1571
+ runtimeMode: 'local',
1572
+ authRequiredForApi: false,
1573
+ workspaceId: 'workspace-local-active'
1574
+ })
1575
+ });
1576
+ }
1577
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1578
+ return Promise.resolve({
1579
+ ok: true,
1580
+ json: () => Promise.resolve({
1581
+ success: true,
1582
+ state: {
1583
+ cloudSyncEnabled: true,
1584
+ sourceOfTruth: 'cloud',
1585
+ onboardingCompleted: true,
1586
+ pullCursor: ''
1587
+ }
1588
+ })
1589
+ });
1590
+ }
1591
+ if (url.includes('/api/taskforce/tasks')) {
1592
+ tasksRequestCount += 1;
1593
+ if (tasksRequestCount === 1) {
1594
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [localTask] }) });
1595
+ }
1596
+ return Promise.resolve({ ok: false, status: 500, json: () => Promise.resolve({ error: 'refresh failed' }) });
1597
+ }
1598
+ if (url.includes('/api/taskforce/archive'))
1599
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1600
+ if (url.includes('/api/taskforce/categories'))
1601
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1602
+ if (url.includes('/api/taskforce/types'))
1603
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1604
+ if (url.includes('/api/taskforce/priorities'))
1605
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1606
+ if (url.includes('/api/taskforce/approaches'))
1607
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1608
+ if (url.includes('/api/taskforce/specialists'))
1609
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1610
+ if (url.includes('/api/taskforce/workflow-templates'))
1611
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1612
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
1613
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1614
+ }
1615
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
1616
+ return pullGate.then(() => Promise.resolve({
1617
+ ok: true,
1618
+ json: () => Promise.resolve({
1619
+ success: true,
1620
+ changes: [{ op: 'delete', taskId: 'task-delete-immediate-1' }],
1621
+ nextCursor: 'cursor-delete-immediate',
1622
+ hasMore: false
1623
+ })
1624
+ }));
1625
+ }
1626
+ if (url === '/api/taskforce/sync/workspace/apply-local') {
1627
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1628
+ }
1629
+ if (url.includes('/api/taskforce/ui-state'))
1630
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1631
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1632
+ });
1633
+ vi.stubGlobal('fetch', fetchMock);
1634
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1635
+ await waitFor(() => {
1636
+ expect(screen.getByText('1/1')).toBeInTheDocument();
1637
+ });
1638
+ if (releasePull)
1639
+ releasePull();
1640
+ await waitFor(() => {
1641
+ expect(screen.getByText('0/0')).toBeInTheDocument();
1642
+ });
1643
+ });
1644
+ it.skip('retries pull automatically after transient cloud failure and applies changes on reconnect', async () => {
1645
+ const cloudTask = {
1646
+ id: 'task-retry-reconnect-1',
1647
+ title: 'Recovered after retry',
1648
+ description: null,
1649
+ category: 'general',
1650
+ type: 'feature',
1651
+ priority: 2,
1652
+ createdAt: new Date().toISOString(),
1653
+ status: 'task'
1654
+ };
1655
+ let pullAttempt = 0;
1656
+ let tasksFetchCount = 0;
1657
+ const fetchMock = vi.fn((url) => {
1658
+ if (url.includes('/api/taskforce/auth/session')) {
1659
+ return Promise.resolve({
1660
+ ok: true,
1661
+ json: () => Promise.resolve({
1662
+ authenticated: true,
1663
+ runtimeMode: 'local',
1664
+ authRequiredForApi: false
1665
+ })
1666
+ });
1667
+ }
1668
+ if (url.includes('/api/taskforce/config')) {
1669
+ return Promise.resolve({
1670
+ ok: true,
1671
+ json: () => Promise.resolve({
1672
+ ...mockConfig,
1673
+ runtimeMode: 'local',
1674
+ authRequiredForApi: false,
1675
+ workspaceId: 'workspace-local-active'
1676
+ })
1677
+ });
1678
+ }
1679
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1680
+ return Promise.resolve({
1681
+ ok: true,
1682
+ json: () => Promise.resolve({
1683
+ success: true,
1684
+ state: {
1685
+ cloudSyncEnabled: true,
1686
+ sourceOfTruth: 'cloud',
1687
+ onboardingCompleted: true,
1688
+ pullCursor: ''
1689
+ }
1690
+ })
1691
+ });
1692
+ }
1693
+ if (url.includes('/api/taskforce/tasks')) {
1694
+ tasksFetchCount += 1;
1695
+ if (tasksFetchCount >= 2) {
1696
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [cloudTask] }) });
1697
+ }
1698
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
1699
+ }
1700
+ if (url.includes('/api/taskforce/archive'))
1701
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1702
+ if (url.includes('/api/taskforce/categories'))
1703
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1704
+ if (url.includes('/api/taskforce/types'))
1705
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1706
+ if (url.includes('/api/taskforce/priorities'))
1707
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1708
+ if (url.includes('/api/taskforce/approaches'))
1709
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1710
+ if (url.includes('/api/taskforce/specialists'))
1711
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1712
+ if (url.includes('/api/taskforce/workflow-templates'))
1713
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1714
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
1715
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1716
+ }
1717
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
1718
+ pullAttempt += 1;
1719
+ if (pullAttempt === 1) {
1720
+ return Promise.resolve({ ok: false, status: 503, json: () => Promise.resolve({ error: 'unavailable' }) });
1721
+ }
1722
+ return Promise.resolve({
1723
+ ok: true,
1724
+ json: () => Promise.resolve({
1725
+ success: true,
1726
+ changes: [{ op: 'upsert', archived: false, task: cloudTask }],
1727
+ nextCursor: 'cursor-retry-success',
1728
+ hasMore: false
1729
+ })
1730
+ });
1731
+ }
1732
+ if (url === '/api/taskforce/sync/workspace/apply-local') {
1733
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1734
+ }
1735
+ if (url.includes('/api/taskforce/ui-state'))
1736
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1737
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1738
+ });
1739
+ vi.stubGlobal('fetch', fetchMock);
1740
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1741
+ await waitFor(() => {
1742
+ const firstPull = fetchMock.mock.calls.find((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
1743
+ expect(firstPull).toBeTruthy();
1744
+ });
1745
+ await waitFor(() => {
1746
+ const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
1747
+ expect(pullCalls.length).toBeGreaterThanOrEqual(2);
1748
+ }, { timeout: 5000 });
1749
+ await waitFor(() => {
1750
+ const applyCalls = fetchMock.mock.calls.filter((call) => call[0] === '/api/taskforce/sync/workspace/apply-local');
1751
+ expect(applyCalls.length).toBeGreaterThanOrEqual(1);
1752
+ }, { timeout: 5000 });
1753
+ });
1754
+ it.skip('retries pull automatically after transient apply-local failures and recovers', async () => {
1755
+ const cloudTask = {
1756
+ id: 'task-retry-apply-failure-1',
1757
+ title: 'Apply retry recovered',
1758
+ description: null,
1759
+ category: 'general',
1760
+ type: 'feature',
1761
+ priority: 2,
1762
+ createdAt: new Date().toISOString(),
1763
+ status: 'task'
1764
+ };
1765
+ let applyAttempt = 0;
1766
+ let pullAttempt = 0;
1767
+ const fetchMock = vi.fn((url) => {
1768
+ if (url.includes('/api/taskforce/auth/session')) {
1769
+ return Promise.resolve({
1770
+ ok: true,
1771
+ json: () => Promise.resolve({
1772
+ authenticated: true,
1773
+ runtimeMode: 'local',
1774
+ authRequiredForApi: false
1775
+ })
1776
+ });
1777
+ }
1778
+ if (url.includes('/api/taskforce/config')) {
1779
+ return Promise.resolve({
1780
+ ok: true,
1781
+ json: () => Promise.resolve({
1782
+ ...mockConfig,
1783
+ runtimeMode: 'local',
1784
+ authRequiredForApi: false,
1785
+ workspaceId: 'workspace-local-active'
1786
+ })
1787
+ });
1788
+ }
1789
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1790
+ return Promise.resolve({
1791
+ ok: true,
1792
+ json: () => Promise.resolve({
1793
+ success: true,
1794
+ state: {
1795
+ cloudSyncEnabled: true,
1796
+ sourceOfTruth: 'cloud',
1797
+ onboardingCompleted: true,
1798
+ pullCursor: ''
1799
+ }
1800
+ })
1801
+ });
1802
+ }
1803
+ if (url.includes('/api/taskforce/tasks'))
1804
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
1805
+ if (url.includes('/api/taskforce/archive'))
1806
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1807
+ if (url.includes('/api/taskforce/categories'))
1808
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1809
+ if (url.includes('/api/taskforce/types'))
1810
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1811
+ if (url.includes('/api/taskforce/priorities'))
1812
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1813
+ if (url.includes('/api/taskforce/approaches'))
1814
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1815
+ if (url.includes('/api/taskforce/specialists'))
1816
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1817
+ if (url.includes('/api/taskforce/workflow-templates'))
1818
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1819
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
1820
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1821
+ }
1822
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
1823
+ pullAttempt += 1;
1824
+ return Promise.resolve({
1825
+ ok: true,
1826
+ json: () => Promise.resolve({
1827
+ success: true,
1828
+ changes: [{ op: 'upsert', archived: false, task: cloudTask }],
1829
+ nextCursor: `cursor-apply-retry-${pullAttempt}`,
1830
+ hasMore: false
1831
+ })
1832
+ });
1833
+ }
1834
+ if (url === '/api/taskforce/sync/workspace/apply-local' || url === '/taskforce/api/taskforce/sync/workspace/apply-local') {
1835
+ applyAttempt += 1;
1836
+ if (applyAttempt <= 2) {
1837
+ return Promise.resolve({ ok: false, status: 503, json: () => Promise.resolve({ error: 'temp apply failure' }) });
1838
+ }
1839
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1840
+ }
1841
+ if (url.includes('/api/taskforce/ui-state'))
1842
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1843
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1844
+ });
1845
+ vi.stubGlobal('fetch', fetchMock);
1846
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1847
+ await waitFor(() => {
1848
+ const firstPull = fetchMock.mock.calls.find((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
1849
+ expect(firstPull).toBeTruthy();
1850
+ });
1851
+ await waitFor(() => {
1852
+ const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
1853
+ expect(pullCalls.length).toBeGreaterThanOrEqual(2);
1854
+ }, { timeout: 6000 });
1855
+ await waitFor(() => {
1856
+ const primaryApplyCalls = fetchMock.mock.calls.filter((call) => call[0] === '/api/taskforce/sync/workspace/apply-local');
1857
+ expect(primaryApplyCalls.length).toBeGreaterThanOrEqual(2);
1858
+ }, { timeout: 6000 });
1859
+ });
1860
+ it.skip('reset cursor + full pull triggers a new pull request without cursor param', async () => {
1861
+ const user = userEvent.setup();
1862
+ const pullUrls = [];
1863
+ const fetchMock = vi.fn((url, init) => {
1864
+ if (url.includes('/api/taskforce/auth/session')) {
1865
+ return Promise.resolve({
1866
+ ok: true,
1867
+ json: () => Promise.resolve({
1868
+ authenticated: true,
1869
+ runtimeMode: 'local',
1870
+ authRequiredForApi: false
1871
+ })
1872
+ });
1873
+ }
1874
+ if (url.includes('/api/taskforce/config')) {
1875
+ return Promise.resolve({
1876
+ ok: true,
1877
+ json: () => Promise.resolve({
1878
+ ...mockConfig,
1879
+ runtimeMode: 'local',
1880
+ authRequiredForApi: false,
1881
+ workspaceId: 'workspace-local-active'
1882
+ })
1883
+ });
1884
+ }
1885
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1886
+ return Promise.resolve({
1887
+ ok: true,
1888
+ json: () => Promise.resolve({
1889
+ success: true,
1890
+ state: {
1891
+ cloudSyncEnabled: true,
1892
+ sourceOfTruth: 'cloud',
1893
+ onboardingCompleted: true,
1894
+ pullCursor: 'cursor-existing'
1895
+ }
1896
+ })
1897
+ });
1898
+ }
1899
+ if (url.includes('/api/taskforce/tasks'))
1900
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
1901
+ if (url.includes('/api/taskforce/archive'))
1902
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1903
+ if (url.includes('/api/taskforce/categories'))
1904
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
1905
+ if (url.includes('/api/taskforce/types'))
1906
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
1907
+ if (url.includes('/api/taskforce/priorities'))
1908
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
1909
+ if (url.includes('/api/taskforce/approaches'))
1910
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
1911
+ if (url.includes('/api/taskforce/specialists'))
1912
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
1913
+ if (url.includes('/api/taskforce/workflow-templates'))
1914
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1915
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
1916
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1917
+ }
1918
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
1919
+ pullUrls.push(url);
1920
+ return Promise.resolve({
1921
+ ok: true,
1922
+ json: () => Promise.resolve({
1923
+ success: true,
1924
+ changes: [],
1925
+ nextCursor: 'cursor-next',
1926
+ hasMore: false
1927
+ })
1928
+ });
1929
+ }
1930
+ if (url === '/api/taskforce/sync/workspace/apply-local') {
1931
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1932
+ }
1933
+ if (url.includes('/api/taskforce/ui-state')) {
1934
+ if (init?.method === 'POST') {
1935
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1936
+ }
1937
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
1938
+ }
1939
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1940
+ });
1941
+ vi.stubGlobal('fetch', fetchMock);
1942
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1943
+ await waitFor(() => {
1944
+ expect(pullUrls.length).toBeGreaterThan(0);
1945
+ });
1946
+ expect(pullUrls[0]).toContain('cursor=cursor-existing');
1947
+ await user.click(screen.getByTitle(/Cloud sync status:/i));
1948
+ await waitFor(() => {
1949
+ expect(screen.getByRole('button', { name: 'Reset Cursor + Full Pull' })).toBeInTheDocument();
1950
+ });
1951
+ const pullCountBeforeReset = pullUrls.length;
1952
+ await user.click(screen.getByRole('button', { name: 'Reset Cursor + Full Pull' }));
1953
+ await waitFor(() => {
1954
+ expect(pullUrls.length).toBeGreaterThan(pullCountBeforeReset);
1955
+ });
1956
+ await waitFor(() => {
1957
+ const afterResetPullUrls = pullUrls.slice(pullCountBeforeReset);
1958
+ expect(afterResetPullUrls.length).toBeGreaterThan(0);
1959
+ const hasCursorlessPull = afterResetPullUrls.some((entry) => (entry.includes('workspaceId=workspace-local-active') && !entry.includes('cursor=')));
1960
+ expect(hasCursorlessPull).toBe(true);
1961
+ });
1962
+ });
1963
+ it.skip('handles pull 401 by transitioning to signed-out local auth state', async () => {
1964
+ let authSessionCalls = 0;
1965
+ const fetchMock = vi.fn((url) => {
1966
+ if (url.includes('/api/taskforce/auth/session')) {
1967
+ authSessionCalls += 1;
1968
+ if (authSessionCalls === 1) {
1969
+ return Promise.resolve({
1970
+ ok: true,
1971
+ json: () => Promise.resolve({
1972
+ authenticated: true,
1973
+ runtimeMode: 'local',
1974
+ authRequiredForApi: false
1975
+ })
1976
+ });
1977
+ }
1978
+ return Promise.resolve({
1979
+ ok: true,
1980
+ json: () => Promise.resolve({
1981
+ authenticated: false,
1982
+ runtimeMode: 'local',
1983
+ authRequiredForApi: false
1984
+ })
1985
+ });
1986
+ }
1987
+ if (url.includes('/api/taskforce/config')) {
1988
+ return Promise.resolve({
1989
+ ok: true,
1990
+ json: () => Promise.resolve({
1991
+ ...mockConfig,
1992
+ runtimeMode: 'local',
1993
+ authRequiredForApi: false,
1994
+ workspaceId: 'workspace-local-active'
1995
+ })
1996
+ });
1997
+ }
1998
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1999
+ return Promise.resolve({
2000
+ ok: true,
2001
+ json: () => Promise.resolve({
2002
+ success: true,
2003
+ state: {
2004
+ cloudSyncEnabled: true,
2005
+ sourceOfTruth: 'cloud',
2006
+ onboardingCompleted: true,
2007
+ pullCursor: ''
2008
+ }
2009
+ })
2010
+ });
2011
+ }
2012
+ if (url.includes('/api/taskforce/tasks'))
2013
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
2014
+ if (url.includes('/api/taskforce/archive'))
2015
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
2016
+ if (url.includes('/api/taskforce/categories'))
2017
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
2018
+ if (url.includes('/api/taskforce/types'))
2019
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
2020
+ if (url.includes('/api/taskforce/priorities'))
2021
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
2022
+ if (url.includes('/api/taskforce/approaches'))
2023
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
2024
+ if (url.includes('/api/taskforce/specialists'))
2025
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
2026
+ if (url.includes('/api/taskforce/workflow-templates'))
2027
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
2028
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
2029
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
2030
+ }
2031
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
2032
+ return Promise.resolve({ ok: false, status: 401, json: () => Promise.resolve({ error: 'Unauthorized' }) });
2033
+ }
2034
+ if (url.includes('/api/taskforce/ui-state'))
2035
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
2036
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2037
+ });
2038
+ vi.stubGlobal('fetch', fetchMock);
2039
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
2040
+ await waitFor(() => {
2041
+ expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
2042
+ });
2043
+ });
2044
+ it.skip('does not downgrade workspace context when /workspaces returns default', async () => {
2045
+ const fetchMock = vi.fn((url) => {
2046
+ if (url.includes('/api/taskforce/auth/session')) {
2047
+ return Promise.resolve({
2048
+ ok: true,
2049
+ json: () => Promise.resolve({
2050
+ authenticated: true,
2051
+ runtimeMode: 'cloud',
2052
+ authRequiredForApi: false,
2053
+ workspaceId: 'workspace-cloud-active'
2054
+ })
2055
+ });
2056
+ }
2057
+ if (url.includes('/api/taskforce/config')) {
2058
+ return Promise.resolve({
2059
+ ok: true,
2060
+ json: () => Promise.resolve({
2061
+ ...mockConfig,
2062
+ runtimeMode: 'cloud',
2063
+ authRequiredForApi: false,
2064
+ workspaceId: 'workspace-cloud-active',
2065
+ setupState: {
2066
+ globalSetupState: 'present',
2067
+ workspaceSetupState: 'present',
2068
+ runtimeMode: 'cloud',
2069
+ workspaceId: 'workspace-cloud-active',
2070
+ mode: 'core',
2071
+ forceSetup: false,
2072
+ forceGlobalSetup: false,
2073
+ forceWorkspaceSetup: false,
2074
+ workspace: {
2075
+ id: 'workspace-cloud-active',
2076
+ slug: 'workspace-cloud-active',
2077
+ name: 'Cloud Active',
2078
+ description: null
2079
+ },
2080
+ suggestedWorkspaceName: ''
2081
+ },
2082
+ runtimeCapabilities: {
2083
+ runtimeMode: 'cloud',
2084
+ supportsCloudAuth: true,
2085
+ supportsAdminManagedSetupOverrides: true,
2086
+ supportsPackageScriptSetupOverrides: false
2087
+ }
2088
+ })
2089
+ });
2090
+ }
2091
+ if (url.includes('/api/taskforce/workspaces')) {
2092
+ return Promise.resolve({
2093
+ ok: true,
2094
+ json: () => Promise.resolve({
2095
+ success: true,
2096
+ currentWorkspaceId: 'default',
2097
+ workspaces: [
2098
+ { id: 'workspace-cloud-active', slug: 'workspace-cloud-active', name: 'Cloud Active', role: 'owner' }
2099
+ ]
2100
+ })
2101
+ });
2102
+ }
2103
+ if (url.includes('/api/taskforce/tasks'))
2104
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
2105
+ if (url.includes('/api/taskforce/archive'))
2106
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
2107
+ if (url.includes('/api/taskforce/categories'))
2108
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
2109
+ if (url.includes('/api/taskforce/types'))
2110
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
2111
+ if (url.includes('/api/taskforce/priorities'))
2112
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
2113
+ if (url.includes('/api/taskforce/approaches'))
2114
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
2115
+ if (url.includes('/api/taskforce/specialists'))
2116
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
2117
+ if (url.includes('/api/taskforce/workflow-templates'))
2118
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
2119
+ if (url.includes('/api/taskforce/ui-state'))
2120
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
2121
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2122
+ });
2123
+ vi.stubGlobal('fetch', fetchMock);
2124
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
2125
+ await waitFor(() => {
2126
+ expect(fetchMock.mock.calls.some((call) => typeof call[0] === 'string' && String(call[0]).includes('/api/taskforce/workspaces'))).toBe(true);
2127
+ });
2128
+ await waitFor(() => {
2129
+ expect(screen.getByTitle('Workspace ID: workspace-cloud-active')).toBeInTheDocument();
2130
+ });
2131
+ });
2132
+ });