@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.
- package/dist/Taskforce.module.css +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- 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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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('
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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: '
|
|
327
|
+
gate: 'missing_entitlement',
|
|
300
328
|
hasEntitlement: false,
|
|
301
329
|
canAccessApp: false,
|
|
302
|
-
planSelectionRequired:
|
|
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('
|
|
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
|
-
|
|
348
|
-
|
|
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.
|
|
358
|
-
|
|
359
|
-
|
|
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 {
|
|
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
|
|
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 {
|