[Web] Enable Quota System for AI Features (#31544)

* feat: migrate from aiErrorAssist naming for disabling AI features to aiFeatures.enabled to avoid confusion

feat: keep aiErrorAssistant as setting on user object until migration is run

* feat: migrate writefull.enabled unset to instead use promotionSet false

* feat: updating to use quota based system for AI usage

* feat: hide relevant sections of quota system behind split test

* feat: ship onAiFreeTrial instead of free quota amount to project meta

* fix: renaming splitTestEnabledForUser to featureFlagEnabledForUser

* fix: v1_personal should have free trial amount of ai quota

* fix: onAiFreeTrial in projectController should account for anonymous users with no features

* feat: fixing marketing exports for ai quotas

* feat: update features epoch

* feat: move to quota tiers, and map tier to numeric allowance within rateLimiters

GitOrigin-RevId: 17763447965aae5777053b783d2601517bfe6b12
This commit is contained in:
Jimmy Domagala-Tang
2026-02-24 09:06:31 +00:00
committed by Copybot
parent 8fb5b0ed05
commit 92463fb3e2
18 changed files with 112 additions and 14 deletions
@@ -30,6 +30,13 @@ describe('InstitutionsFeatures', function () {
default: {
institutionPlanCode: ctx.institutionPlanCode,
overleaf: {},
writefull: {
quotaTierGranted: 'unlimited',
},
aiFeatures: {
freeTrialQuota: 'basic',
unlimitedQuota: 'unlimited',
},
},
}))
@@ -52,10 +59,18 @@ describe('InstitutionsFeatures', function () {
}
ctx.testFeatures = { features: { institution: 'all' } }
ctx.testFeaturesWithAiAddon = {
features: { institution: 'all', aiErrorAssistant: true },
features: {
institution: 'all',
aiUsageQuota: 'unlimited',
aiErrorAssistant: true,
},
}
ctx.testFeaturesWithNoAddon = {
features: { institution: 'all', aiErrorAssistant: false },
features: {
institution: 'all',
aiUsageQuota: 'basic',
aiErrorAssistant: false,
},
}
})
@@ -40,6 +40,7 @@ describe('ProjectListController', function () {
theme: 'textmate',
mode: 'none',
},
aiFeatures: { enabled: false },
}
ctx.users = {
'user-1': {
@@ -74,6 +74,11 @@ describe('FeaturesUpdater', function () {
},
writefull: {
overleafApiUrl: 'https://www.writefull.com',
quotaTierGranted: 'unlimited',
},
aiFeatures: {
freeTrialQuota: 'basic',
unlimitedQuota: 'unlimited',
},
}
@@ -323,6 +328,7 @@ describe('FeaturesUpdater', function () {
default: 'features',
individual: 'features',
aiErrorAssistant: true,
aiUsageQuota: 'unlimited',
})
})
})
@@ -341,6 +347,7 @@ describe('FeaturesUpdater', function () {
expect(features).to.deep.equal({
default: 'features',
aiErrorAssistant: true,
aiUsageQuota: 'unlimited',
})
})
})
@@ -66,6 +66,10 @@ describe('UserGetter', function () {
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
reconfirmNotificationDays: 14,
aiFeatures: {
freeTrialQuota: 'basic',
unlimitedQuota: 'unlimited',
},
}),
}))
@@ -1312,8 +1316,8 @@ describe('UserGetter', function () {
it('should take into account features overrides from modules', async function (ctx) {
// this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
const bundleFeatures = { aiErrorAssistant: true }
ctx.fakeUser.features = { aiErrorAssistant: false }
const bundleFeatures = { aiUsageQuota: 'unlimited' }
ctx.fakeUser.features = { aiUsageQuota: 'basic' }
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([bundleFeatures])
const features = await ctx.UserGetter.promises.getUserFeatures(
ctx.fakeUser._id