@taskforcehq/taskforce 0.3.306 → 0.3.308
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/README.md +0 -2
- package/dist/Taskforce.module.css +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +221 -82
- package/dist/components/views/PlanComparisonPage.test.js +268 -14
- package/dist/components/views/PlansPage.js +16 -10
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +30 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/PlanningEntities.test.js +366 -4
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +204 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +781 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.js +58 -15
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +328 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +5 -0
- package/dist/server/routes/authSupport.js +13 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +116 -63
- package/dist/server/routes/billing.test.js +162 -11
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +972 -23
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/agent-logos/ChatGPT.png +0 -0
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
- package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
- package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
- package/dist/ui/assets/index-MAHKvFqp.js +4 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/og-image.png +0 -0
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
|
@@ -157,6 +157,7 @@ function setupBillingSchema(db) {
|
|
|
157
157
|
tenant_id TEXT NOT NULL,
|
|
158
158
|
plan_version_id TEXT NOT NULL,
|
|
159
159
|
billing_interval TEXT NOT NULL,
|
|
160
|
+
pricing_audience TEXT NOT NULL DEFAULT 'public',
|
|
160
161
|
stripe_price_id TEXT NOT NULL,
|
|
161
162
|
pricing_type TEXT NOT NULL DEFAULT 'stripe',
|
|
162
163
|
active INTEGER NOT NULL DEFAULT 1,
|
|
@@ -164,10 +165,10 @@ function setupBillingSchema(db) {
|
|
|
164
165
|
currency TEXT,
|
|
165
166
|
created_at TEXT NOT NULL,
|
|
166
167
|
updated_at TEXT NOT NULL,
|
|
167
|
-
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, stripe_price_id)
|
|
168
|
+
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id)
|
|
168
169
|
);
|
|
169
170
|
CREATE UNIQUE INDEX idx_billing_price_mappings_active_interval
|
|
170
|
-
ON billing_price_mappings (tenant_id, plan_version_id, billing_interval)
|
|
171
|
+
ON billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience)
|
|
171
172
|
WHERE active = 1;
|
|
172
173
|
CREATE TABLE plan_versions (
|
|
173
174
|
plan_version_id TEXT PRIMARY KEY,
|
|
@@ -257,6 +258,7 @@ function readMockPlanVersionFeatures(db, tenantId, planVersionId) {
|
|
|
257
258
|
function buildBillingRouteHandlers(db, opts) {
|
|
258
259
|
const routes = new Map();
|
|
259
260
|
const tenantId = 'default';
|
|
261
|
+
const siteSettings = opts?.siteSettings || {};
|
|
260
262
|
const resolvePlanIdForVersion = (planVersionIdRaw) => {
|
|
261
263
|
const planVersionId = String(planVersionIdRaw || '').trim();
|
|
262
264
|
if (!planVersionId)
|
|
@@ -431,6 +433,9 @@ function buildBillingRouteHandlers(db, opts) {
|
|
|
431
433
|
planId: planVersionId.startsWith('pro') ? 'pro' : 'starter',
|
|
432
434
|
entitlementState: 'active'
|
|
433
435
|
}),
|
|
436
|
+
getGlobalSettings: () => ({
|
|
437
|
+
site: siteSettings
|
|
438
|
+
}),
|
|
434
439
|
resolveUserIdentity: () => opts?.userIdentity === undefined
|
|
435
440
|
? {
|
|
436
441
|
userId: 'u1',
|
|
@@ -1001,6 +1006,39 @@ describe('Billing Webhook Routes', () => {
|
|
|
1001
1006
|
expect(args.customer).toBeUndefined();
|
|
1002
1007
|
expect(args.customer_email).toBe('owner@example.com');
|
|
1003
1008
|
});
|
|
1009
|
+
it('creates checkout with the campaign price mapping when requested', async () => {
|
|
1010
|
+
const now = new Date().toISOString();
|
|
1011
|
+
db.prepare(`
|
|
1012
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1013
|
+
VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
|
|
1014
|
+
`).run(now, now);
|
|
1015
|
+
db.prepare(`
|
|
1016
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, active, created_at, updated_at)
|
|
1017
|
+
VALUES ('default', 'collab-v1', 'month', 'public', 'price_collab_month', 1, ?, ?)
|
|
1018
|
+
`).run(now, now);
|
|
1019
|
+
db.prepare(`
|
|
1020
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, active, created_at, updated_at)
|
|
1021
|
+
VALUES ('default', 'collab-v1', 'month', 'campaign', 'price_collab_campaign_month', 1, ?, ?)
|
|
1022
|
+
`).run(now, now);
|
|
1023
|
+
const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
|
|
1024
|
+
const req = createMockReq(JSON.stringify({
|
|
1025
|
+
planVersionId: 'collab-v1',
|
|
1026
|
+
interval: 'month',
|
|
1027
|
+
pricingAudience: 'campaign'
|
|
1028
|
+
}), {
|
|
1029
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
1030
|
+
host: 'app.taskforcehq.ai'
|
|
1031
|
+
});
|
|
1032
|
+
const res = createMockRes();
|
|
1033
|
+
await checkoutHandler?.(req, res, {});
|
|
1034
|
+
expect(res.statusCode).toBe(200);
|
|
1035
|
+
const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
|
|
1036
|
+
expect(args.line_items?.[0]?.price).toBe('price_collab_campaign_month');
|
|
1037
|
+
expect(String(args.success_url || '')).toContain('pricingAudience=campaign');
|
|
1038
|
+
expect(String(args.cancel_url || '')).toContain('pricingAudience=campaign');
|
|
1039
|
+
expect(args.metadata?.pricingAudience).toBe('campaign');
|
|
1040
|
+
expect(args.subscription_data?.metadata?.pricingAudience).toBe('campaign');
|
|
1041
|
+
});
|
|
1004
1042
|
it('uses per-version trial days when creating checkout', async () => {
|
|
1005
1043
|
const now = new Date().toISOString();
|
|
1006
1044
|
db.prepare(`
|
|
@@ -2546,6 +2584,55 @@ describe('Billing Webhook Routes', () => {
|
|
|
2546
2584
|
{ stripe_price_id: 'price_old', active: 0, unit_amount: 1500 }
|
|
2547
2585
|
]);
|
|
2548
2586
|
});
|
|
2587
|
+
it('keeps campaign price mappings separate from public mappings in admin routes', async () => {
|
|
2588
|
+
const now = new Date().toISOString();
|
|
2589
|
+
db.prepare(`
|
|
2590
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2591
|
+
VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
|
|
2592
|
+
`).run(now, now);
|
|
2593
|
+
db.prepare(`
|
|
2594
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
|
|
2595
|
+
VALUES ('default', 'collab-v1', 'month', 'public', 'price_public', 'stripe', 1, 1500, 'usd', ?, ?)
|
|
2596
|
+
`).run(now, now);
|
|
2597
|
+
mockPriceRetrieve.mockResolvedValueOnce({
|
|
2598
|
+
id: 'price_campaign',
|
|
2599
|
+
active: true,
|
|
2600
|
+
currency: 'usd',
|
|
2601
|
+
unit_amount: 900,
|
|
2602
|
+
recurring: { interval: 'month' }
|
|
2603
|
+
});
|
|
2604
|
+
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
2605
|
+
const upsertReq = createMockReq(JSON.stringify({
|
|
2606
|
+
planVersionId: 'collab-v1',
|
|
2607
|
+
interval: 'month',
|
|
2608
|
+
pricingAudience: 'campaign',
|
|
2609
|
+
stripePriceId: 'price_campaign',
|
|
2610
|
+
active: true
|
|
2611
|
+
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2612
|
+
const upsertRes = createMockRes();
|
|
2613
|
+
await upsertRoute(upsertReq, upsertRes, {});
|
|
2614
|
+
expect(upsertRes.statusCode).toBe(200);
|
|
2615
|
+
expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
|
|
2616
|
+
mapping: expect.objectContaining({
|
|
2617
|
+
plan_version_id: 'collab-v1',
|
|
2618
|
+
billing_interval: 'month',
|
|
2619
|
+
pricing_audience: 'campaign',
|
|
2620
|
+
stripe_price_id: 'price_campaign',
|
|
2621
|
+
active: 1,
|
|
2622
|
+
unit_amount: 900
|
|
2623
|
+
})
|
|
2624
|
+
}));
|
|
2625
|
+
const rows = db.prepare(`
|
|
2626
|
+
SELECT pricing_audience, stripe_price_id, active
|
|
2627
|
+
FROM billing_price_mappings
|
|
2628
|
+
WHERE tenant_id = 'default' AND plan_version_id = 'collab-v1' AND billing_interval = 'month'
|
|
2629
|
+
ORDER BY pricing_audience ASC
|
|
2630
|
+
`).all();
|
|
2631
|
+
expect(rows).toEqual([
|
|
2632
|
+
{ pricing_audience: 'campaign', stripe_price_id: 'price_campaign', active: 1 },
|
|
2633
|
+
{ pricing_audience: 'public', stripe_price_id: 'price_public', active: 1 }
|
|
2634
|
+
]);
|
|
2635
|
+
});
|
|
2549
2636
|
it('rejects manual reuse of a Stripe price id across multiple plan versions', async () => {
|
|
2550
2637
|
const now = new Date().toISOString();
|
|
2551
2638
|
db.prepare(`
|
|
@@ -2723,6 +2810,10 @@ describe('Billing Webhook Routes', () => {
|
|
|
2723
2810
|
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
|
|
2724
2811
|
VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1500, 'usd', ?, ?)
|
|
2725
2812
|
`).run(now, now);
|
|
2813
|
+
db.prepare(`
|
|
2814
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
|
|
2815
|
+
VALUES ('default', 'pro-v1', 'month', 'campaign', 'price_pro_campaign_month', 'stripe', 1, 900, 'usd', ?, ?)
|
|
2816
|
+
`).run(now, now);
|
|
2726
2817
|
db.prepare(`
|
|
2727
2818
|
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
|
|
2728
2819
|
VALUES ('default', 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, NULL, ?, ?)
|
|
@@ -2733,6 +2824,8 @@ describe('Billing Webhook Routes', () => {
|
|
|
2733
2824
|
expect(res.statusCode).toBe(200);
|
|
2734
2825
|
const body = JSON.parse(res.body);
|
|
2735
2826
|
expect(body.plans).toBeInstanceOf(Array);
|
|
2827
|
+
expect(body.pageTitle).toBe('Subscription Plans');
|
|
2828
|
+
expect(body.pageDescription).toBe('Manage your subscription and explore available features. Your current plan is highlighted below.');
|
|
2736
2829
|
expect(body.pricingSource).toEqual({
|
|
2737
2830
|
environment: 'production',
|
|
2738
2831
|
runtimeMode: 'cloud'
|
|
@@ -2751,6 +2844,14 @@ describe('Billing Webhook Routes', () => {
|
|
|
2751
2844
|
expect(monthPricing.currency).toBe('usd');
|
|
2752
2845
|
expect(monthPricing.stripePriceId).toBe('price_pro_month');
|
|
2753
2846
|
expect(proPlan.pricing.year).toBeNull();
|
|
2847
|
+
expect(proPlan.campaignPricing.month).toEqual(expect.objectContaining({
|
|
2848
|
+
pricingType: 'stripe',
|
|
2849
|
+
unitAmount: 900,
|
|
2850
|
+
currency: 'usd',
|
|
2851
|
+
stripePriceId: 'price_pro_campaign_month',
|
|
2852
|
+
interval: 'month'
|
|
2853
|
+
}));
|
|
2854
|
+
expect(proPlan.campaignPricing.year).toBeNull();
|
|
2754
2855
|
// Free plan is explicitly free, not "missing"
|
|
2755
2856
|
expect(freePlan.pricing.month.pricingType).toBe('free');
|
|
2756
2857
|
expect(freePlan.pricing.month.unitAmount).toBe(0);
|
|
@@ -2791,6 +2892,40 @@ describe('Billing Webhook Routes', () => {
|
|
|
2791
2892
|
const secondBody = JSON.parse(secondRes.body);
|
|
2792
2893
|
expect(secondBody.plans.find((p) => p.planId === 'pro')?.pricing?.month?.unitAmount).toBe(2400);
|
|
2793
2894
|
});
|
|
2895
|
+
it('invalidates cached public pricing when pricing page header copy changes', async () => {
|
|
2896
|
+
const now = new Date().toISOString();
|
|
2897
|
+
const siteSettings = {};
|
|
2898
|
+
routeHandlers = buildBillingRouteHandlers(db, { siteSettings });
|
|
2899
|
+
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|
|
2900
|
+
expect(pricingRoute).toBeTypeOf('function');
|
|
2901
|
+
db.prepare(`
|
|
2902
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2903
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
|
|
2904
|
+
`).run(now, now);
|
|
2905
|
+
db.prepare(`
|
|
2906
|
+
INSERT INTO billing_price_mappings (
|
|
2907
|
+
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
2908
|
+
) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
|
|
2909
|
+
`).run(now, now);
|
|
2910
|
+
const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2911
|
+
const firstRes = createMockRes();
|
|
2912
|
+
await pricingRoute(firstReq, firstRes, {});
|
|
2913
|
+
expect(firstRes.statusCode).toBe(200);
|
|
2914
|
+
expect(JSON.parse(firstRes.body)).toMatchObject({
|
|
2915
|
+
pageTitle: 'Subscription Plans',
|
|
2916
|
+
pageDescription: 'Manage your subscription and explore available features. Your current plan is highlighted below.'
|
|
2917
|
+
});
|
|
2918
|
+
siteSettings.pricingPageTitle = 'Choose Your Plan';
|
|
2919
|
+
siteSettings.pricingPageDescription = 'Compare plans and pick the right fit for your team.';
|
|
2920
|
+
const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2921
|
+
const secondRes = createMockRes();
|
|
2922
|
+
await pricingRoute(secondReq, secondRes, {});
|
|
2923
|
+
expect(secondRes.statusCode).toBe(200);
|
|
2924
|
+
expect(JSON.parse(secondRes.body)).toMatchObject({
|
|
2925
|
+
pageTitle: 'Choose Your Plan',
|
|
2926
|
+
pageDescription: 'Compare plans and pick the right fit for your team.'
|
|
2927
|
+
});
|
|
2928
|
+
});
|
|
2794
2929
|
it('invalidates cached public pricing when plan features change', async () => {
|
|
2795
2930
|
const now = new Date().toISOString();
|
|
2796
2931
|
routeHandlers = buildBillingRouteHandlers(db);
|
|
@@ -2844,7 +2979,8 @@ describe('Billing Webhook Routes', () => {
|
|
|
2844
2979
|
});
|
|
2845
2980
|
it('includes public feature copy and invalidates cached public pricing when catalog overrides change', async () => {
|
|
2846
2981
|
const now = new Date().toISOString();
|
|
2847
|
-
|
|
2982
|
+
const siteSettings = {};
|
|
2983
|
+
routeHandlers = buildBillingRouteHandlers(db, { siteSettings });
|
|
2848
2984
|
const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
|
|
2849
2985
|
expect(pricingRoute).toBeTypeOf('function');
|
|
2850
2986
|
db.prepare(`
|
|
@@ -2874,15 +3010,11 @@ describe('Billing Webhook Routes', () => {
|
|
|
2874
3010
|
featureKey: 'collaboration.ai_profiles',
|
|
2875
3011
|
label: 'AI Profiles',
|
|
2876
3012
|
publicLabel: 'AI teammates',
|
|
2877
|
-
publicDescription: 'Add reusable AI teammates to your workspace.'
|
|
3013
|
+
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
3014
|
+
publicDescriptionVisible: false
|
|
2878
3015
|
})
|
|
2879
3016
|
]);
|
|
2880
|
-
|
|
2881
|
-
db.prepare(`
|
|
2882
|
-
UPDATE plan_feature_catalog_overrides
|
|
2883
|
-
SET public_label = 'AI copilots', updated_at = ?
|
|
2884
|
-
WHERE tenant_id = 'default' AND feature_key = 'collaboration.ai_profiles'
|
|
2885
|
-
`).run(updatedAt);
|
|
3017
|
+
siteSettings.pricingPageShowPublicDescriptions = true;
|
|
2886
3018
|
const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2887
3019
|
const secondRes = createMockRes();
|
|
2888
3020
|
await pricingRoute(secondReq, secondRes, {});
|
|
@@ -2891,7 +3023,25 @@ describe('Billing Webhook Routes', () => {
|
|
|
2891
3023
|
expect(secondBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
|
|
2892
3024
|
expect.objectContaining({
|
|
2893
3025
|
featureKey: 'collaboration.ai_profiles',
|
|
2894
|
-
|
|
3026
|
+
publicDescriptionVisible: true
|
|
3027
|
+
})
|
|
3028
|
+
]);
|
|
3029
|
+
const updatedAt = new Date(Date.now() + 1000).toISOString();
|
|
3030
|
+
db.prepare(`
|
|
3031
|
+
UPDATE plan_feature_catalog_overrides
|
|
3032
|
+
SET public_label = 'AI copilots', updated_at = ?
|
|
3033
|
+
WHERE tenant_id = 'default' AND feature_key = 'collaboration.ai_profiles'
|
|
3034
|
+
`).run(updatedAt);
|
|
3035
|
+
const thirdReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
3036
|
+
const thirdRes = createMockRes();
|
|
3037
|
+
await pricingRoute(thirdReq, thirdRes, {});
|
|
3038
|
+
expect(thirdRes.statusCode).toBe(200);
|
|
3039
|
+
const thirdBody = JSON.parse(thirdRes.body);
|
|
3040
|
+
expect(thirdBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
|
|
3041
|
+
expect.objectContaining({
|
|
3042
|
+
featureKey: 'collaboration.ai_profiles',
|
|
3043
|
+
publicLabel: 'AI copilots',
|
|
3044
|
+
publicDescriptionVisible: true
|
|
2895
3045
|
})
|
|
2896
3046
|
]);
|
|
2897
3047
|
});
|
|
@@ -2949,6 +3099,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
2949
3099
|
await pricingRoute(req, res, {});
|
|
2950
3100
|
expect(res.statusCode).toBe(200);
|
|
2951
3101
|
const body = JSON.parse(res.body);
|
|
3102
|
+
expect(body.pageTitle).toBe('Subscription Plans');
|
|
2952
3103
|
expect(body.pricingSource).toEqual({
|
|
2953
3104
|
environment: 'performance',
|
|
2954
3105
|
runtimeMode: 'cloud'
|
|
@@ -474,6 +474,14 @@ export function registerDocumentRoutes(deps) {
|
|
|
474
474
|
if (shouldProxyInlineUserAvatar && await proxyCloudUserProfileAvatar(req, res, normalizedRelative, contentTypeForExt)) {
|
|
475
475
|
return;
|
|
476
476
|
}
|
|
477
|
+
if (shouldProxyInlineAiAvatar && filePath && fs.existsSync(filePath)) {
|
|
478
|
+
res.writeHead(200, {
|
|
479
|
+
'Content-Type': contentTypeForExt(ext),
|
|
480
|
+
'Content-Disposition': contentDisposition
|
|
481
|
+
});
|
|
482
|
+
res.end(fs.readFileSync(filePath));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
477
485
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
478
486
|
res.end('Context file not found');
|
|
479
487
|
return;
|
|
@@ -873,6 +881,7 @@ export function registerDocumentRoutes(deps) {
|
|
|
873
881
|
}));
|
|
874
882
|
return;
|
|
875
883
|
}
|
|
884
|
+
const attachedAt = new Date().toISOString();
|
|
876
885
|
const payload = buildTaskAttachmentPayload(asset.storageKey, {
|
|
877
886
|
caption: getCanonicalDocumentName({
|
|
878
887
|
assetId: asset.assetId,
|
|
@@ -883,6 +892,7 @@ export function registerDocumentRoutes(deps) {
|
|
|
883
892
|
}),
|
|
884
893
|
assetId: asset.assetId,
|
|
885
894
|
taskId: targetId,
|
|
895
|
+
timestamp: attachedAt,
|
|
886
896
|
});
|
|
887
897
|
if (!payload) {
|
|
888
898
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -216,16 +216,6 @@ export function makeResolveAuthenticatedMutationUserId(core, context, requestWor
|
|
|
216
216
|
const explicitUserId = String(requestUserId(req) || '').trim();
|
|
217
217
|
if (explicitUserId && explicitUserId !== 'anonymous')
|
|
218
218
|
return explicitUserId;
|
|
219
|
-
if (requestRuntimeMode(req) === 'local') {
|
|
220
|
-
const workspaceId = String(requestWorkspaceId(req) || '').trim() || 'default';
|
|
221
|
-
const fallbackUser = core.listWorkspaceUsers(workspaceId).find((user) => {
|
|
222
|
-
const status = String(user.status || '').trim().toLowerCase();
|
|
223
|
-
return status === 'active' && user.disabled !== true;
|
|
224
|
-
});
|
|
225
|
-
const fallbackUserId = String(fallbackUser?.userId || '').trim();
|
|
226
|
-
if (fallbackUserId)
|
|
227
|
-
return fallbackUserId;
|
|
228
|
-
}
|
|
229
219
|
throw new TaskforceRuleError('Authenticated user required for mutation attribution.', 401, 'MUTATION_AUTH_REQUIRED');
|
|
230
220
|
};
|
|
231
221
|
}
|
|
@@ -1676,6 +1676,227 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
1676
1676
|
})
|
|
1677
1677
|
]);
|
|
1678
1678
|
});
|
|
1679
|
+
it('round-trips planning attachment links through pull and apply-local routes', async () => {
|
|
1680
|
+
const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-planning-link-cloud-');
|
|
1681
|
+
const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-planning-link-local-');
|
|
1682
|
+
const cloudCore = new TaskforceCore({ projectRoot: cloudProjectRoot, storagePath: '.taskforce' });
|
|
1683
|
+
seedAuthenticatedWorkspace(cloudCore);
|
|
1684
|
+
cloudCore.createInitiative({
|
|
1685
|
+
id: 'initiative-cloud-link-roundtrip-1',
|
|
1686
|
+
title: 'Cloud initiative attachment target',
|
|
1687
|
+
createdAt: '2026-03-18T09:00:00.000Z',
|
|
1688
|
+
updatedAt: '2026-03-18T09:00:00.000Z'
|
|
1689
|
+
}, workspaceId);
|
|
1690
|
+
cloudCore.createWorkstream({
|
|
1691
|
+
id: 'workstream-cloud-link-roundtrip-1',
|
|
1692
|
+
initiativeId: 'initiative-cloud-link-roundtrip-1',
|
|
1693
|
+
title: 'Cloud workstream attachment target',
|
|
1694
|
+
createdAt: '2026-03-18T09:00:00.000Z',
|
|
1695
|
+
updatedAt: '2026-03-18T09:00:00.000Z'
|
|
1696
|
+
}, workspaceId);
|
|
1697
|
+
const cloudBasePath = cloudCore.getPaths().basePath;
|
|
1698
|
+
const cloudStore = new WorkspaceAssetStore(cloudCore.getDatabaseAdapter(), cloudCore.getTenantId());
|
|
1699
|
+
const markdownPath = `workspaces/${workspaceId}/assets/documents/asset-cloud-planning-link-plan.md`;
|
|
1700
|
+
const filePath = `workspaces/${workspaceId}/assets/files/asset-cloud-planning-link-notes.txt`;
|
|
1701
|
+
fs.mkdirSync(path.dirname(path.join(cloudBasePath, markdownPath)), { recursive: true });
|
|
1702
|
+
fs.mkdirSync(path.dirname(path.join(cloudBasePath, filePath)), { recursive: true });
|
|
1703
|
+
fs.writeFileSync(path.join(cloudBasePath, markdownPath), '# planning sync\n', 'utf8');
|
|
1704
|
+
fs.writeFileSync(path.join(cloudBasePath, filePath), 'planning attachment', 'utf8');
|
|
1705
|
+
cloudStore.upsertAsset({
|
|
1706
|
+
assetId: 'asset-cloud-planning-link-doc-1',
|
|
1707
|
+
workspaceId,
|
|
1708
|
+
kind: 'document',
|
|
1709
|
+
originalFilename: 'plan.md',
|
|
1710
|
+
mimeType: 'text/markdown',
|
|
1711
|
+
storageProvider: 'local',
|
|
1712
|
+
storageKey: markdownPath,
|
|
1713
|
+
contentSha256: 'planning-doc-roundtrip-hash',
|
|
1714
|
+
sizeBytes: '# planning sync\n'.length,
|
|
1715
|
+
documentId: 'doc-cloud-planning-link-1',
|
|
1716
|
+
version: 1,
|
|
1717
|
+
isLatest: true,
|
|
1718
|
+
createdAt: '2026-03-18T09:00:00.000Z',
|
|
1719
|
+
updatedAt: '2026-03-18T09:00:00.000Z'
|
|
1720
|
+
});
|
|
1721
|
+
cloudStore.upsertLink({
|
|
1722
|
+
workspaceId,
|
|
1723
|
+
assetId: 'asset-cloud-planning-link-doc-1',
|
|
1724
|
+
targetType: 'initiative',
|
|
1725
|
+
targetId: 'initiative-cloud-link-roundtrip-1',
|
|
1726
|
+
role: 'attachment',
|
|
1727
|
+
updatedAt: '2026-03-18T10:00:00.000Z'
|
|
1728
|
+
});
|
|
1729
|
+
cloudStore.upsertAsset({
|
|
1730
|
+
assetId: 'asset-cloud-planning-link-file-1',
|
|
1731
|
+
workspaceId,
|
|
1732
|
+
kind: 'file',
|
|
1733
|
+
originalFilename: 'notes.txt',
|
|
1734
|
+
mimeType: 'text/plain',
|
|
1735
|
+
storageProvider: 'local',
|
|
1736
|
+
storageKey: filePath,
|
|
1737
|
+
contentSha256: 'planning-file-roundtrip-hash',
|
|
1738
|
+
sizeBytes: 'planning attachment'.length,
|
|
1739
|
+
isLatest: true,
|
|
1740
|
+
createdAt: '2026-03-18T09:00:00.000Z',
|
|
1741
|
+
updatedAt: '2026-03-18T09:00:00.000Z'
|
|
1742
|
+
});
|
|
1743
|
+
cloudStore.upsertLink({
|
|
1744
|
+
workspaceId,
|
|
1745
|
+
assetId: 'asset-cloud-planning-link-file-1',
|
|
1746
|
+
targetType: 'workstream',
|
|
1747
|
+
targetId: 'workstream-cloud-link-roundtrip-1',
|
|
1748
|
+
role: 'attachment',
|
|
1749
|
+
updatedAt: '2026-03-18T10:00:01.000Z'
|
|
1750
|
+
});
|
|
1751
|
+
const cloudRoutes = createRoutes(cloudCore, { authConfig });
|
|
1752
|
+
const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', cloudRoutes);
|
|
1753
|
+
expect(pullMatch).toBeTruthy();
|
|
1754
|
+
const cookie = createAuthCookie(workspaceId);
|
|
1755
|
+
const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=50&cursor=eyJ1IjoiMjAyNi0wMy0xOFQwOTozMDowMC4wMDBaIiwiaWQiOiJzZWVkIn0', undefined, { cookie });
|
|
1756
|
+
const pullRes = createMockRes();
|
|
1757
|
+
await pullMatch?.handler(pullReq, pullRes, {});
|
|
1758
|
+
expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1759
|
+
const pullBody = readJsonBody(pullRes);
|
|
1760
|
+
const pulledChanges = Array.isArray(pullBody?.changes) ? pullBody.changes : [];
|
|
1761
|
+
expect(pulledChanges).toEqual(expect.arrayContaining([
|
|
1762
|
+
expect.objectContaining({
|
|
1763
|
+
op: 'document-upsert',
|
|
1764
|
+
path: markdownPath,
|
|
1765
|
+
assetId: 'asset-cloud-planning-link-doc-1',
|
|
1766
|
+
linkTargetType: 'initiative',
|
|
1767
|
+
linkTargetId: 'initiative-cloud-link-roundtrip-1',
|
|
1768
|
+
originalFilename: 'plan.md',
|
|
1769
|
+
linkRole: 'attachment'
|
|
1770
|
+
}),
|
|
1771
|
+
expect.objectContaining({
|
|
1772
|
+
op: 'asset-upsert',
|
|
1773
|
+
path: filePath,
|
|
1774
|
+
assetId: 'asset-cloud-planning-link-file-1',
|
|
1775
|
+
linkTargetType: 'workstream',
|
|
1776
|
+
linkTargetId: 'workstream-cloud-link-roundtrip-1',
|
|
1777
|
+
originalFilename: 'notes.txt',
|
|
1778
|
+
linkRole: 'attachment'
|
|
1779
|
+
})
|
|
1780
|
+
]));
|
|
1781
|
+
const localCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
|
|
1782
|
+
localCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
1783
|
+
localCore.createInitiative({
|
|
1784
|
+
id: 'initiative-cloud-link-roundtrip-1',
|
|
1785
|
+
title: 'Local initiative attachment target',
|
|
1786
|
+
createdAt: '2026-03-18T09:00:00.000Z',
|
|
1787
|
+
updatedAt: '2026-03-18T09:00:00.000Z'
|
|
1788
|
+
}, workspaceId);
|
|
1789
|
+
localCore.createWorkstream({
|
|
1790
|
+
id: 'workstream-cloud-link-roundtrip-1',
|
|
1791
|
+
initiativeId: 'initiative-cloud-link-roundtrip-1',
|
|
1792
|
+
title: 'Local workstream attachment target',
|
|
1793
|
+
createdAt: '2026-03-18T09:00:00.000Z',
|
|
1794
|
+
updatedAt: '2026-03-18T09:00:00.000Z'
|
|
1795
|
+
}, workspaceId);
|
|
1796
|
+
const localRoutes = createRoutes(localCore, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
1797
|
+
const applyMatch = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', localRoutes);
|
|
1798
|
+
expect(applyMatch).toBeTruthy();
|
|
1799
|
+
const applyReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: pulledChanges }, { 'x-taskforce-workspace-id': workspaceId });
|
|
1800
|
+
const applyRes = createMockRes();
|
|
1801
|
+
await applyMatch?.handler(applyReq, applyRes, {});
|
|
1802
|
+
expect(applyRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1803
|
+
const reloadedLocalCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
|
|
1804
|
+
const reloadedStore = new WorkspaceAssetStore(reloadedLocalCore.getDatabaseAdapter(), reloadedLocalCore.getTenantId());
|
|
1805
|
+
expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, markdownPath), 'utf8')).toBe('# planning sync\n');
|
|
1806
|
+
expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, filePath), 'utf8')).toBe('planning attachment');
|
|
1807
|
+
expect(reloadedStore.listLinksForAsset('asset-cloud-planning-link-doc-1', workspaceId)).toEqual([
|
|
1808
|
+
expect.objectContaining({
|
|
1809
|
+
targetType: 'initiative',
|
|
1810
|
+
targetId: 'initiative-cloud-link-roundtrip-1',
|
|
1811
|
+
taskId: null,
|
|
1812
|
+
role: 'attachment',
|
|
1813
|
+
deletedAt: null
|
|
1814
|
+
})
|
|
1815
|
+
]);
|
|
1816
|
+
expect(reloadedStore.listLinksForAsset('asset-cloud-planning-link-file-1', workspaceId)).toEqual([
|
|
1817
|
+
expect.objectContaining({
|
|
1818
|
+
targetType: 'workstream',
|
|
1819
|
+
targetId: 'workstream-cloud-link-roundtrip-1',
|
|
1820
|
+
taskId: null,
|
|
1821
|
+
role: 'attachment',
|
|
1822
|
+
deletedAt: null
|
|
1823
|
+
})
|
|
1824
|
+
]);
|
|
1825
|
+
});
|
|
1826
|
+
it('pushes document attachment links when only the link metadata changed locally', async () => {
|
|
1827
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-doc-link-only-push-');
|
|
1828
|
+
const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1829
|
+
seedAuthenticatedWorkspace(seedCore);
|
|
1830
|
+
seedCore.createTask({
|
|
1831
|
+
id: 'task-doc-link-only-push-1',
|
|
1832
|
+
title: 'Document link target',
|
|
1833
|
+
status: 'task'
|
|
1834
|
+
}, {
|
|
1835
|
+
workspaceId,
|
|
1836
|
+
allowClientProvidedId: true
|
|
1837
|
+
});
|
|
1838
|
+
const seedBasePath = seedCore.getPaths().basePath;
|
|
1839
|
+
const relativeDocPath = `workspaces/${workspaceId}/assets/documents/asset-doc-link-only-push-plan.md`;
|
|
1840
|
+
const absoluteDocPath = path.join(seedBasePath, relativeDocPath);
|
|
1841
|
+
const content = '# already synced\n';
|
|
1842
|
+
fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
|
|
1843
|
+
fs.writeFileSync(absoluteDocPath, content, 'utf8');
|
|
1844
|
+
const contentHash = createHash('sha256').update(content).digest('hex');
|
|
1845
|
+
seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, contentHash, '2026-03-18T13:30:00.000Z');
|
|
1846
|
+
const seedStore = new WorkspaceAssetStore(seedCore.getDatabaseAdapter(), seedCore.getTenantId());
|
|
1847
|
+
seedStore.upsertAsset({
|
|
1848
|
+
assetId: 'asset-doc-link-only-push-1',
|
|
1849
|
+
workspaceId,
|
|
1850
|
+
kind: 'document',
|
|
1851
|
+
originalFilename: 'plan.md',
|
|
1852
|
+
mimeType: 'text/markdown',
|
|
1853
|
+
storageProvider: 'local',
|
|
1854
|
+
storageKey: relativeDocPath,
|
|
1855
|
+
contentSha256: contentHash,
|
|
1856
|
+
sizeBytes: Buffer.byteLength(content, 'utf8'),
|
|
1857
|
+
documentId: 'doc-link-only-push-1',
|
|
1858
|
+
version: 1,
|
|
1859
|
+
isLatest: true,
|
|
1860
|
+
createdAt: '2026-03-18T13:30:00.000Z',
|
|
1861
|
+
updatedAt: '2026-03-18T13:30:00.000Z'
|
|
1862
|
+
});
|
|
1863
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1864
|
+
const routes = createRoutes(core, { authConfig });
|
|
1865
|
+
const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
|
|
1866
|
+
expect(pushMatch).toBeTruthy();
|
|
1867
|
+
const req = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
|
|
1868
|
+
workspaceId,
|
|
1869
|
+
idempotencyKey: 'doc-link-only-push-1',
|
|
1870
|
+
changes: [
|
|
1871
|
+
{
|
|
1872
|
+
op: 'document-upsert',
|
|
1873
|
+
path: relativeDocPath,
|
|
1874
|
+
updatedAt: '2026-03-18T14:45:00.000Z',
|
|
1875
|
+
content,
|
|
1876
|
+
assetId: 'asset-doc-link-only-push-1',
|
|
1877
|
+
documentId: 'doc-link-only-push-1',
|
|
1878
|
+
referenceNumber: 12,
|
|
1879
|
+
version: 1,
|
|
1880
|
+
taskId: 'task-doc-link-only-push-1',
|
|
1881
|
+
originalFilename: 'plan.md',
|
|
1882
|
+
linkRole: 'attachment'
|
|
1883
|
+
}
|
|
1884
|
+
]
|
|
1885
|
+
}, { cookie: createAuthCookie(workspaceId) });
|
|
1886
|
+
const res = createMockRes();
|
|
1887
|
+
await pushMatch?.handler(req, res, {});
|
|
1888
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1889
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1890
|
+
const reloadedStore = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
1891
|
+
expect(reloadedStore.listLinksForAsset('asset-doc-link-only-push-1', workspaceId)).toEqual([
|
|
1892
|
+
expect.objectContaining({
|
|
1893
|
+
taskId: 'task-doc-link-only-push-1',
|
|
1894
|
+
role: 'attachment',
|
|
1895
|
+
updatedAt: '2026-03-18T14:45:00.000Z',
|
|
1896
|
+
deletedAt: null
|
|
1897
|
+
})
|
|
1898
|
+
]);
|
|
1899
|
+
});
|
|
1679
1900
|
it('propagates document-delete through authenticated push and omits deleted doc from pull payload', async () => {
|
|
1680
1901
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-doc-delete-');
|
|
1681
1902
|
const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
@@ -3,7 +3,7 @@ import { parseJsonBody } from './primitives.js';
|
|
|
3
3
|
import { normalizeTaskDataRelativePath, ObjectStorageClient } from '../../storage/objectStorageClient.js';
|
|
4
4
|
import { WorkspaceAssetStore, } from '../../storage/workspaceAssetStore.js';
|
|
5
5
|
import { resolveSessionCookieAttributesForRequest } from '../auth.js';
|
|
6
|
-
import { isAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
|
|
6
|
+
import { isAiProfileSurfaceType, parseAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
|
|
7
7
|
import { isAiProfileSeatScope } from '../../shared/aiProfileSeatScope.js';
|
|
8
8
|
import { serializeBinaryAssetSyncPayload, serializeDocumentSyncPayload, } from '../../sync/contentSyncPayload.js';
|
|
9
9
|
import { createContentSyncState } from '../../sync/contentSyncState.js';
|
|
@@ -52,6 +52,8 @@ function normalizeIncomingAiProfileSurfaceType(value, context) {
|
|
|
52
52
|
const normalized = String(value || '').trim();
|
|
53
53
|
if (!normalized)
|
|
54
54
|
return null;
|
|
55
|
+
if (normalized === 'other')
|
|
56
|
+
return parseAiProfileSurfaceType(normalized);
|
|
55
57
|
if (!isAiProfileSurfaceType(normalized)) {
|
|
56
58
|
throw createSyncRouteError(`Invalid ai profile surfaceType "${normalized}"`, {
|
|
57
59
|
...context,
|
|
@@ -79,11 +81,17 @@ function normalizeTaskEventSyncPayload(raw, workspaceId, sourceWatermark) {
|
|
|
79
81
|
return null;
|
|
80
82
|
const source = raw;
|
|
81
83
|
const id = String(source.id || '').trim();
|
|
84
|
+
const entityType = source.entityType === 'initiative' || source.entityType === 'workstream'
|
|
85
|
+
? source.entityType
|
|
86
|
+
: source.entity_type === 'initiative' || source.entity_type === 'workstream'
|
|
87
|
+
? source.entity_type
|
|
88
|
+
: 'task';
|
|
82
89
|
const taskId = String(source.taskId || source.task_id || '').trim();
|
|
90
|
+
const entityId = String(source.entityId || source.entity_id || taskId).trim();
|
|
83
91
|
const action = String(source.action || '').trim();
|
|
84
92
|
const actor = String(source.actor || 'system').trim() || 'system';
|
|
85
93
|
const createdAt = String(source.createdAt || source.created_at || sourceWatermark || '').trim();
|
|
86
|
-
if (!id || !
|
|
94
|
+
if (!id || !entityId || !action || !Number.isFinite(Date.parse(createdAt)))
|
|
87
95
|
return null;
|
|
88
96
|
const actorType = source.actorType === 'ai' || source.actorType === 'human' || source.actorType === 'system'
|
|
89
97
|
? source.actorType
|
|
@@ -99,8 +107,10 @@ function normalizeTaskEventSyncPayload(raw, workspaceId, sourceWatermark) {
|
|
|
99
107
|
: {};
|
|
100
108
|
return {
|
|
101
109
|
id,
|
|
102
|
-
taskId,
|
|
103
110
|
workspaceId,
|
|
111
|
+
entityType,
|
|
112
|
+
entityId,
|
|
113
|
+
taskId: entityType === 'task' ? (taskId || entityId) : undefined,
|
|
104
114
|
action,
|
|
105
115
|
actor,
|
|
106
116
|
actorType,
|
|
@@ -59,6 +59,8 @@ export function registerWorkspaceSnapshotSyncRoutes({ addRoute, core, context, r
|
|
|
59
59
|
profile.profileToken,
|
|
60
60
|
profile.avatarUrl ?? '',
|
|
61
61
|
profile.avatarSourceUrl ?? '',
|
|
62
|
+
profile.avatarRevision ?? 0,
|
|
63
|
+
profile.avatarUpdatedAt ?? '',
|
|
62
64
|
profile.surfaceType ?? '',
|
|
63
65
|
profile.seatScope ?? '',
|
|
64
66
|
profile.archivedAt ?? '',
|