@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -6,7 +6,16 @@ import { TaskforceCore } from './Taskforce';
6
6
  function makeProjectRoot() {
7
7
  return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-system-admin-'));
8
8
  }
9
- function seedPlans(core) {
9
+ function insertFreePriceMapping(core, planVersionId) {
10
+ const db = core.db;
11
+ const now = new Date().toISOString();
12
+ db.prepare(`
13
+ INSERT OR IGNORE INTO billing_price_mappings (
14
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
15
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
16
+ `).run(core.getTenantId(), planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
17
+ }
18
+ function seedPlanCatalog(core) {
10
19
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
11
20
  core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
12
21
  core.updatePlanVersion({
@@ -29,10 +38,16 @@ function seedPlans(core) {
29
38
  { featureKey: 'workspace.switching', access: 'enabled' }
30
39
  ]
31
40
  });
41
+ insertFreePriceMapping(core, 'starter-v1');
42
+ }
43
+ function seedPlans(core) {
44
+ seedPlanCatalog(core);
32
45
  core.updateGlobalSettings({
33
- auth: {
34
- signupMonetizationStrategy: 'auto_assign_default',
35
- defaultSignupPlanVersionId: 'starter-v1'
46
+ billing: {
47
+ onboardingPolicy: {
48
+ mode: 'auto_grant_free',
49
+ planVersionId: 'starter-v1'
50
+ }
36
51
  }
37
52
  });
38
53
  }
@@ -98,7 +113,7 @@ describe('TaskforceCore system admin model', () => {
98
113
  fs.rmSync(projectRoot, { recursive: true, force: true });
99
114
  }
100
115
  });
101
- it('uses configured signup default plan version for registration when auto-assign config is valid', () => {
116
+ it('starts a configured onboarding trial when the onboarding policy is trial-based', () => {
102
117
  const projectRoot = makeProjectRoot();
103
118
  const core = openCore(projectRoot, { seedDefaultPlans: true });
104
119
  try {
@@ -106,7 +121,8 @@ describe('TaskforceCore system admin model', () => {
106
121
  planId: 'collab',
107
122
  versionNumber: 2,
108
123
  isDefault: true,
109
- seatLimit: 10
124
+ seatLimit: 10,
125
+ trialDays: 14
110
126
  });
111
127
  core.updatePlanVersionFeatures({
112
128
  planVersionId: 'collab-v2',
@@ -124,9 +140,11 @@ describe('TaskforceCore system admin model', () => {
124
140
  seatLimit: null
125
141
  });
126
142
  core.updateGlobalSettings({
127
- auth: {
128
- signupMonetizationStrategy: 'auto_assign_default',
129
- defaultSignupPlanVersionId: 'collab-v2'
143
+ billing: {
144
+ onboardingPolicy: {
145
+ mode: 'auto_start_trial',
146
+ planVersionId: 'collab-v2'
147
+ }
130
148
  }
131
149
  });
132
150
  const created = core.registerPasswordUser({
@@ -143,22 +161,25 @@ describe('TaskforceCore system admin model', () => {
143
161
  `).get(core.getTenantId(), created.userId);
144
162
  expect(entitlement.planVersionId).toBe('collab-v2');
145
163
  expect(entitlement.planId).toBe('collab');
164
+ expect(entitlement.state).toBe('trialing');
146
165
  expect(billing?.tier).toBe('collab');
147
- expect(billing?.entitlement_state).toBe('active');
166
+ expect(billing?.entitlement_state).toBe('trialing');
148
167
  }
149
168
  finally {
150
169
  core.close();
151
170
  fs.rmSync(projectRoot, { recursive: true, force: true });
152
171
  }
153
172
  });
154
- it('fails registration when auto-assign signup config is stale or invalid', () => {
173
+ it('fails registration when the onboarding policy is stale or invalid', () => {
155
174
  const projectRoot = makeProjectRoot();
156
175
  const core = openCore(projectRoot, { seedDefaultPlans: true });
157
176
  try {
158
177
  core.updateGlobalSettings({
159
- auth: {
160
- signupMonetizationStrategy: 'auto_assign_default',
161
- defaultSignupPlanVersionId: 'missing-v9'
178
+ billing: {
179
+ onboardingPolicy: {
180
+ mode: 'auto_grant_free',
181
+ planVersionId: 'missing-v9'
182
+ }
162
183
  }
163
184
  });
164
185
  expect(() => core.registerPasswordUser({
@@ -176,9 +197,11 @@ describe('TaskforceCore system admin model', () => {
176
197
  const core = openCore(projectRoot, { seedDefaultPlans: true });
177
198
  try {
178
199
  core.updateGlobalSettings({
179
- auth: {
180
- signupMonetizationStrategy: 'plan_selection_required',
181
- defaultSignupPlanVersionId: null
200
+ billing: {
201
+ onboardingPolicy: {
202
+ mode: 'require_plan_selection',
203
+ planVersionId: null
204
+ }
182
205
  }
183
206
  });
184
207
  const created = core.registerPasswordUser({
@@ -186,7 +209,7 @@ describe('TaskforceCore system admin model', () => {
186
209
  password: 'password123'
187
210
  });
188
211
  expect(created.planSelectionRequired).toBe(true);
189
- expect(core.hasAccountEntitlement(created.userId)).toBe(false);
212
+ expect(core.hasAccountEntitlement(created.userId)).toBe(true);
190
213
  const db = core.db;
191
214
  const billing = db.prepare(`
192
215
  SELECT entitlement_state, tier
@@ -198,7 +221,7 @@ describe('TaskforceCore system admin model', () => {
198
221
  expect(String(billing?.tier || '')).toBe('');
199
222
  expect(core.getAccountAccessStatus(created.userId)).toMatchObject({
200
223
  gate: 'plan_selection_required',
201
- hasEntitlement: false,
224
+ hasEntitlement: true,
202
225
  canAccessApp: false,
203
226
  canAccessPlans: true
204
227
  });
@@ -245,7 +268,7 @@ describe('TaskforceCore system admin model', () => {
245
268
  fs.rmSync(projectRoot, { recursive: true, force: true });
246
269
  }
247
270
  });
248
- it('assigns the explicitly selected signup plan version instead of the default signup plan', () => {
271
+ it('assigns the explicitly selected plan version instead of the configured onboarding plan', () => {
249
272
  const projectRoot = makeProjectRoot();
250
273
  const core = openCore(projectRoot, { seedDefaultPlans: true });
251
274
  try {
@@ -255,11 +278,14 @@ describe('TaskforceCore system admin model', () => {
255
278
  versionNumber: 1,
256
279
  seatLimit: 1
257
280
  });
281
+ insertFreePriceMapping(core, 'free-v1');
258
282
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'archived' });
259
283
  core.updateGlobalSettings({
260
- auth: {
261
- signupMonetizationStrategy: 'auto_assign_default',
262
- defaultSignupPlanVersionId: 'starter-v1'
284
+ billing: {
285
+ onboardingPolicy: {
286
+ mode: 'auto_grant_free',
287
+ planVersionId: 'starter-v1'
288
+ }
263
289
  }
264
290
  });
265
291
  const created = core.registerPasswordUser({
@@ -279,14 +305,16 @@ describe('TaskforceCore system admin model', () => {
279
305
  fs.rmSync(projectRoot, { recursive: true, force: true });
280
306
  }
281
307
  });
282
- it('treats accounts without an entitlement or pending-selection marker as plan selection required', () => {
308
+ it('treats accounts without an entitlement or pending-selection marker as missing entitlement', () => {
283
309
  const projectRoot = makeProjectRoot();
284
310
  const core = openCore(projectRoot, { seedDefaultPlans: true });
285
311
  try {
286
312
  core.updateGlobalSettings({
287
- auth: {
288
- signupMonetizationStrategy: 'plan_selection_required',
289
- defaultSignupPlanVersionId: null
313
+ billing: {
314
+ onboardingPolicy: {
315
+ mode: 'require_plan_selection',
316
+ planVersionId: null
317
+ }
290
318
  }
291
319
  });
292
320
  const now = new Date().toISOString();
@@ -296,10 +324,10 @@ describe('TaskforceCore system admin model', () => {
296
324
  VALUES (?, ?, ?, ?, ?, ?, ?, 'user', 1, 0, ?, ?)
297
325
  `).run('user-missing-entitlement', 'default', 'broken@example.com', 'Broken', 'password', 'broken@example.com', 'hash', now, now);
298
326
  expect(core.getAccountAccessStatus('user-missing-entitlement')).toMatchObject({
299
- gate: 'plan_selection_required',
327
+ gate: 'missing_entitlement',
300
328
  hasEntitlement: false,
301
329
  canAccessApp: false,
302
- planSelectionRequired: true
330
+ planSelectionRequired: false
303
331
  });
304
332
  }
305
333
  finally {
@@ -339,13 +367,16 @@ describe('TaskforceCore system admin model', () => {
339
367
  fs.rmSync(projectRoot, { recursive: true, force: true });
340
368
  }
341
369
  });
342
- it('persists signup monetization defaults during startup for existing installs', () => {
370
+ it('preserves require_plan_selection onboarding policy across restart', () => {
343
371
  const projectRoot = makeProjectRoot();
344
372
  const initial = openCore(projectRoot, { seedDefaultPlans: true });
345
373
  try {
346
374
  initial.updateGlobalSettings({
347
- auth: {
348
- allowSelfRegistration: true
375
+ billing: {
376
+ onboardingPolicy: {
377
+ mode: 'require_plan_selection',
378
+ planVersionId: null
379
+ }
349
380
  }
350
381
  });
351
382
  }
@@ -354,70 +385,9 @@ describe('TaskforceCore system admin model', () => {
354
385
  }
355
386
  const reopened = openCore(projectRoot, { seedDefaultPlans: false });
356
387
  try {
357
- expect(reopened.getSignupMonetizationSettings()).toEqual({
358
- strategy: 'auto_assign_default',
359
- defaultSignupPlanVersionId: 'starter-v1'
360
- });
361
- expect(reopened.getGlobalSettings().auth).toMatchObject({
362
- signupMonetizationStrategy: 'auto_assign_default',
363
- defaultSignupPlanVersionId: 'starter-v1'
364
- });
365
- }
366
- finally {
367
- reopened.close();
368
- fs.rmSync(projectRoot, { recursive: true, force: true });
369
- }
370
- });
371
- it('repairs signup monetization defaults on the same startup pass that repairs missing plan defaults', () => {
372
- const projectRoot = makeProjectRoot();
373
- const initial = openCore(projectRoot, { seedDefaultPlans: true });
374
- try {
375
- initial.updateGlobalSettings({
376
- auth: {
377
- allowSelfRegistration: true
378
- }
379
- });
380
- const db = initial.db;
381
- db.prepare(`
382
- UPDATE plan_versions
383
- SET is_default = 0
384
- WHERE tenant_id = ? AND plan_id = ?
385
- `).run('default', 'starter');
386
- }
387
- finally {
388
- initial.close();
389
- }
390
- const reopened = openCore(projectRoot, { seedDefaultPlans: false });
391
- try {
392
- expect(reopened.getSignupMonetizationSettings()).toEqual({
393
- strategy: 'auto_assign_default',
394
- defaultSignupPlanVersionId: 'starter-v1'
395
- });
396
- }
397
- finally {
398
- reopened.close();
399
- fs.rmSync(projectRoot, { recursive: true, force: true });
400
- }
401
- });
402
- it('does not inject a default signup plan version when strategy is plan selection required', () => {
403
- const projectRoot = makeProjectRoot();
404
- const initial = openCore(projectRoot, { seedDefaultPlans: true });
405
- try {
406
- initial.updateGlobalSettings({
407
- auth: {
408
- signupMonetizationStrategy: 'plan_selection_required',
409
- defaultSignupPlanVersionId: null
410
- }
411
- });
412
- }
413
- finally {
414
- initial.close();
415
- }
416
- const reopened = openCore(projectRoot, { seedDefaultPlans: false });
417
- try {
418
- expect(reopened.getSignupMonetizationSettings()).toEqual({
419
- strategy: 'plan_selection_required',
420
- defaultSignupPlanVersionId: null
388
+ expect(reopened.getOnboardingPolicy()).toEqual({
389
+ mode: 'require_plan_selection',
390
+ planVersionId: null
421
391
  });
422
392
  }
423
393
  finally {
@@ -663,6 +633,126 @@ describe('TaskforceCore system admin model', () => {
663
633
  fs.rmSync(projectRoot, { recursive: true, force: true });
664
634
  }
665
635
  });
636
+ it('purges a non-production test user and clears active ownership references', () => {
637
+ const projectRoot = makeProjectRoot();
638
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
639
+ try {
640
+ const admin = core.registerPasswordUser({
641
+ email: 'admin@example.com',
642
+ password: 'password123'
643
+ });
644
+ const member = core.registerPasswordUser({
645
+ email: 'member@example.com',
646
+ password: 'password123'
647
+ });
648
+ const workspaceId = member.workspaceId || 'default';
649
+ const db = core.db;
650
+ db.prepare(`
651
+ UPDATE workspace_memberships
652
+ SET role = 'member', updated_at = ?
653
+ WHERE tenant_id = ? AND user_id = ?
654
+ `).run(new Date().toISOString(), 'default', member.userId);
655
+ const task = core.createTask({
656
+ title: 'Signup test task',
657
+ assignee: member.userId
658
+ }, { workspaceId });
659
+ const initiative = core.createInitiative({
660
+ title: 'Signup initiative',
661
+ ownerId: member.userId
662
+ }, workspaceId);
663
+ const workstream = core.createWorkstream({
664
+ title: 'Signup workstream',
665
+ ownerId: member.userId
666
+ }, workspaceId);
667
+ const now = new Date().toISOString();
668
+ db.prepare(`
669
+ INSERT INTO ui_state (tenant_id, workspace_id, user_id, state_key, payload, updated_at)
670
+ VALUES (?, ?, ?, ?, ?, ?)
671
+ `).run('default', workspaceId, member.userId, 'admin.users', '{"open":true}', now);
672
+ db.prepare(`
673
+ INSERT INTO user_preferences (tenant_id, user_id, prefs_key, payload, updated_at)
674
+ VALUES (?, ?, ?, ?, ?)
675
+ `).run('default', member.userId, 'profile', '{"theme":"light"}', now);
676
+ const summary = core.purgeSystemUser({
677
+ targetUserId: member.userId,
678
+ actorUserId: admin.userId,
679
+ emailConfirmation: 'member@example.com'
680
+ });
681
+ expect(summary).toMatchObject({
682
+ targetUserId: member.userId,
683
+ email: 'member@example.com',
684
+ unassignedTaskCount: 1,
685
+ clearedInitiativeOwnerCount: 1,
686
+ clearedWorkstreamOwnerCount: 1,
687
+ deletedUiStateCount: 1,
688
+ deletedUserPreferenceCount: 1,
689
+ deletedUserCount: 1
690
+ });
691
+ expect(db.prepare(`SELECT assignee FROM tasks WHERE id = ?`).get(task.id)).toMatchObject({
692
+ assignee: 'unassigned'
693
+ });
694
+ expect(db.prepare(`SELECT owner_id FROM initiatives WHERE id = ?`).get(initiative.id)).toMatchObject({
695
+ owner_id: null
696
+ });
697
+ expect(db.prepare(`SELECT owner_id FROM workstreams WHERE id = ?`).get(workstream.id)).toMatchObject({
698
+ owner_id: null
699
+ });
700
+ expect(db.prepare(`SELECT COUNT(*) AS count FROM users WHERE tenant_id = ? AND id = ?`).get('default', member.userId)).toMatchObject({
701
+ count: 0
702
+ });
703
+ expect(db.prepare(`SELECT COUNT(*) AS count FROM account_entitlements WHERE tenant_id = ? AND account_id = ?`).get('default', member.userId)).toMatchObject({
704
+ count: 0
705
+ });
706
+ expect(db.prepare(`SELECT COUNT(*) AS count FROM ui_state WHERE tenant_id = ? AND user_id = ?`).get('default', member.userId)).toMatchObject({
707
+ count: 0
708
+ });
709
+ expect(db.prepare(`SELECT COUNT(*) AS count FROM user_preferences WHERE tenant_id = ? AND user_id = ?`).get('default', member.userId)).toMatchObject({
710
+ count: 0
711
+ });
712
+ }
713
+ finally {
714
+ core.close();
715
+ fs.rmSync(projectRoot, { recursive: true, force: true });
716
+ }
717
+ });
718
+ it('blocks purging a user with active commercial billing state', () => {
719
+ const projectRoot = makeProjectRoot();
720
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
721
+ try {
722
+ const admin = core.registerPasswordUser({
723
+ email: 'admin@example.com',
724
+ password: 'password123'
725
+ });
726
+ const member = core.registerPasswordUser({
727
+ email: 'member@example.com',
728
+ password: 'password123'
729
+ });
730
+ const db = core.db;
731
+ const now = new Date().toISOString();
732
+ db.prepare(`
733
+ UPDATE workspace_memberships
734
+ SET role = 'member', updated_at = ?
735
+ WHERE tenant_id = ? AND user_id = ?
736
+ `).run(now, 'default', member.userId);
737
+ db.prepare(`
738
+ INSERT INTO user_billing (
739
+ user_id, tenant_id, stripe_customer_id, entitlement_state, tier, created_at, updated_at
740
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
741
+ ON CONFLICT (user_id) DO UPDATE SET
742
+ stripe_customer_id = excluded.stripe_customer_id,
743
+ updated_at = excluded.updated_at
744
+ `).run(member.userId, 'default', 'cus_test_123', 'active', 'starter', now, now);
745
+ expect(() => core.purgeSystemUser({
746
+ targetUserId: member.userId,
747
+ actorUserId: admin.userId,
748
+ emailConfirmation: 'member@example.com'
749
+ })).toThrowError(/active commercial billing state/i);
750
+ }
751
+ finally {
752
+ core.close();
753
+ fs.rmSync(projectRoot, { recursive: true, force: true });
754
+ }
755
+ });
666
756
  it('creates v1 automatically when a new plan is created', () => {
667
757
  const projectRoot = makeProjectRoot();
668
758
  const core = openCore(projectRoot, { seedDefaultPlans: true });
@@ -1,5 +1,6 @@
1
1
  import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
2
- import type { Comment, TaskActivityItem, TaskActor, TaskEvent, TaskEventDetails } from './Taskforce.js';
2
+ import type { TaskActor } from '../shared/taskActor.js';
3
+ import type { Comment, TaskActivityItem, TaskEvent, TaskEventDetails } from './Taskforce.js';
3
4
  export interface TaskActivityServiceDeps {
4
5
  db: DatabaseAdapter;
5
6
  tenantId: string;
@@ -7,6 +8,7 @@ export interface TaskActivityServiceDeps {
7
8
  normalizeWorkspaceId: (workspaceId: string | undefined) => string;
8
9
  resolveWorkspaceActor: (workspaceId: string, actorRef: unknown) => TaskActor | null;
9
10
  }
11
+ export declare function listWorkspaceTaskEventsService(deps: TaskActivityServiceDeps, workspaceId: string): TaskEvent[];
10
12
  export declare function replaceTaskCommentsService(deps: TaskActivityServiceDeps, taskId: string, workspaceId: string, comments: Comment[]): void;
11
13
  export declare function listTaskEventsInternalService(deps: TaskActivityServiceDeps, taskId: string, workspaceId: string): TaskEvent[];
12
14
  export declare function buildTaskActivityService(comments: Comment[], taskEvents: TaskEvent[]): TaskActivityItem[];
@@ -19,3 +21,4 @@ export declare function addTaskEventService(deps: TaskActivityServiceDeps, input
19
21
  details?: TaskEventDetails;
20
22
  }): void;
21
23
  export declare function replaceTaskEventsService(deps: TaskActivityServiceDeps, taskId: string, workspaceId: string, taskEvents: TaskEvent[]): void;
24
+ export declare function upsertTaskEventForSyncService(deps: TaskActivityServiceDeps, event: TaskEvent): TaskEvent;
@@ -39,6 +39,17 @@ function mapTaskEventRow(deps, row) {
39
39
  actorProfile: actor === 'system' ? null : deps.resolveWorkspaceActor(workspaceId, actor)
40
40
  };
41
41
  }
42
+ export function listWorkspaceTaskEventsService(deps, workspaceId) {
43
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
44
+ const rows = deps.db.prepare(`
45
+ SELECT id, task_id, workspace_id, action, actor, actor_type, details_json, created_at
46
+ FROM task_events
47
+ WHERE tenant_id = ?
48
+ AND workspace_id = ?
49
+ ORDER BY created_at ASC, id ASC
50
+ `).all(deps.tenantId, normalizedWorkspaceId);
51
+ return rows.map((row) => mapTaskEventRow(deps, row));
52
+ }
42
53
  export function replaceTaskCommentsService(deps, taskId, workspaceId, comments) {
43
54
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
44
55
  deps.db.prepare('DELETE FROM comments WHERE tenant_id = ? AND workspace_id = ? AND task_id = ?')
@@ -128,3 +139,37 @@ export function replaceTaskEventsService(deps, taskId, workspaceId, taskEvents)
128
139
  insertTaskEvent.run(event.id, deps.tenantId, normalizedWorkspaceId, taskId, event.action, event.actor, event.actorType, JSON.stringify(normalizeTaskEventDetails(event.details)), event.createdAt);
129
140
  }
130
141
  }
142
+ export function upsertTaskEventForSyncService(deps, event) {
143
+ const workspaceId = deps.normalizeWorkspaceId(event.workspaceId);
144
+ const normalizedEvent = {
145
+ id: String(event.id || '').trim(),
146
+ taskId: String(event.taskId || '').trim(),
147
+ workspaceId,
148
+ action: String(event.action || '').trim(),
149
+ actor: String(event.actor || 'system').trim() || 'system',
150
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
151
+ ? event.actorType
152
+ : 'system',
153
+ createdAt: String(event.createdAt || '').trim() || new Date().toISOString(),
154
+ details: normalizeTaskEventDetails(event.details)
155
+ };
156
+ deps.db.prepare(`
157
+ INSERT INTO task_events (
158
+ id, tenant_id, workspace_id, task_id, action, actor, actor_type, details_json, created_at
159
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
160
+ ON CONFLICT (id) DO UPDATE SET
161
+ workspace_id = excluded.workspace_id,
162
+ task_id = excluded.task_id,
163
+ action = excluded.action,
164
+ actor = excluded.actor,
165
+ actor_type = excluded.actor_type,
166
+ details_json = excluded.details_json,
167
+ created_at = excluded.created_at
168
+ `).run(normalizedEvent.id, deps.tenantId, workspaceId, normalizedEvent.taskId, normalizedEvent.action, normalizedEvent.actor, normalizedEvent.actorType, JSON.stringify(normalizedEvent.details), normalizedEvent.createdAt);
169
+ return {
170
+ ...normalizedEvent,
171
+ actorProfile: normalizedEvent.actor === 'system'
172
+ ? null
173
+ : deps.resolveWorkspaceActor(workspaceId, normalizedEvent.actor)
174
+ };
175
+ }
@@ -92,7 +92,7 @@ describe('TaskforceCore task audit timeline', () => {
92
92
  cleanup();
93
93
  }
94
94
  });
95
- it('suppresses task events for autosave-style updates when skipTaskEvents is true', () => {
95
+ it('suppresses task events when skipTaskEvents is explicitly true', () => {
96
96
  const { core, cleanup } = makeCore();
97
97
  try {
98
98
  const task = core.createTask({ title: 'Autosave task', createdBy: 'user' }, { workspaceId: 'workspace-a', actorRef: 'user' });
@@ -110,6 +110,57 @@ describe('TaskforceCore task audit timeline', () => {
110
110
  cleanup();
111
111
  }
112
112
  });
113
+ it('suppresses title and description autosave events while preserving the saved task content', () => {
114
+ const { core, cleanup } = makeCore();
115
+ try {
116
+ const task = core.createTask({ title: 'Autosave title task', createdBy: 'user' }, { workspaceId: 'workspace-a', actorRef: 'user' });
117
+ core.updateTask(task.id, {
118
+ title: 'Autosave title task updated',
119
+ description: 'Draft body'
120
+ }, 'workspace-a', {
121
+ actorRef: 'user',
122
+ saveSource: 'autosave'
123
+ });
124
+ const events = core.listTaskEvents(task.id, 'workspace-a');
125
+ const hydrated = core.getTask(task.id, 'workspace-a');
126
+ expect(events.map((event) => event.action)).toEqual(['task-created']);
127
+ expect(hydrated?.title).toBe('Autosave title task updated');
128
+ expect(hydrated?.description).toBe('Draft body');
129
+ expect(hydrated?.activity?.some((item) => item.type === 'event' && item.event.action === 'task-updated')).toBe(false);
130
+ }
131
+ finally {
132
+ cleanup();
133
+ }
134
+ });
135
+ it('keeps meaningful autosave updates in the activity feed while filtering noisy text changes', () => {
136
+ const { core, cleanup } = makeCore();
137
+ try {
138
+ const task = core.createTask({ title: 'Autosave assignee task', createdBy: 'user' }, { workspaceId: 'workspace-a', actorRef: 'user' });
139
+ core.updateTask(task.id, {
140
+ title: 'Autosave assignee task updated',
141
+ assignee: 'member-1'
142
+ }, 'workspace-a', {
143
+ actorRef: 'user',
144
+ saveSource: 'autosave'
145
+ });
146
+ const assignmentEvent = core.listTaskEvents(task.id, 'workspace-a').find((event) => event.action === 'task-updated' && event.details?.changes?.assignee);
147
+ expect(assignmentEvent).toEqual(expect.objectContaining({
148
+ action: 'task-updated',
149
+ details: {
150
+ changes: {
151
+ assignee: {
152
+ from: 'unassigned',
153
+ to: 'member-1',
154
+ },
155
+ },
156
+ },
157
+ }));
158
+ expect(assignmentEvent?.details?.changes?.title).toBeUndefined();
159
+ }
160
+ finally {
161
+ cleanup();
162
+ }
163
+ });
113
164
  it('captures assignment changes in the core task update event details', () => {
114
165
  const { core, cleanup } = makeCore();
115
166
  try {