Merge pull request #23972 from overleaf/jdt-grant-assist-via-wf-set-trait

enable granting of premium error assist based on WF entitlement to bu…

GitOrigin-RevId: 9d21cf8755c881bdc698c0cf9891076ecefd34eb
This commit is contained in:
Jimmy Domagala-Tang 2025-03-11 13:10:28 -04:00 committed by Copybot
parent 19eefebe95
commit 80abd0ac2c
5 changed files with 91 additions and 1 deletions

View file

@ -15,6 +15,7 @@ const metrics = require('@overleaf/metrics')
const { User } = require('../../models/User')
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
const LimitationsManager = require('../Subscription/LimitationsManager')
const FeaturesHelper = require('../Subscription/FeaturesHelper')
const Settings = require('@overleaf/settings')
const AuthorizationManager = require('../Authorization/AuthorizationManager')
const InactiveProjectManager = require('../InactiveData/InactiveProjectManager')
@ -760,6 +761,20 @@ const _ProjectController = {
const isOverleafAssistBundleEnabled =
splitTestAssignments['overleaf-assist-bundle']?.variant === 'enabled'
let fullFeatureSet = user?.features
if (!anonymous) {
// generate users feature set including features added, or overriden via modules
const moduleFeatures =
(await Modules.promises.hooks.fire(
'getModuleProvidedFeatures',
userId
)) || []
fullFeatureSet = FeaturesHelper.computeFeatureSet([
user.features,
...moduleFeatures,
])
}
const isPaywallChangeCompileTimeoutEnabled =
splitTestAssignments['paywall-change-compile-timeout']?.variant ===
'enabled'
@ -802,7 +817,7 @@ const _ProjectController = {
allowedFreeTrial,
hasRecurlySubscription: subscription?.recurlySubscription_id != null,
featureSwitches: user.featureSwitches,
features: user.features,
features: fullFeatureSet,
featureUsage,
refProviders: _.mapValues(user.refProviders, Boolean),
writefull: {

View file

@ -1,6 +1,13 @@
const _ = require('lodash')
const Settings = require('@overleaf/settings')
/**
* merges an array of feature sets to produce a final feature set
*/
function computeFeatureSet(featureSets) {
return featureSets.reduce(mergeFeatures, {})
}
/**
* Merge feature sets coming from different sources
*/
@ -108,6 +115,7 @@ function getMatchedFeatureSet(features) {
module.exports = {
mergeFeatures,
computeFeatureSet,
isFeatureSetBetter,
compareFeatures,
getMatchedFeatureSet,

View file

@ -194,6 +194,7 @@ const UserSchema = new Schema(
writefull: {
enabled: { type: Boolean, default: null },
autoCreatedAccount: { type: Boolean, default: false },
isPremium: { type: Boolean, default: false },
},
aiErrorAssistant: {
enabled: { type: Boolean, default: true },

View file

@ -1121,6 +1121,44 @@ describe('ProjectController', function () {
this.ProjectController.loadEditor(this.req, this.res)
})
})
describe('when fetching the users featureSet', function () {
beforeEach(function () {
this.Modules.promises.hooks.fire = sinon.stub().resolves()
this.user.features = {}
})
it('should take into account features overrides from modules', function (done) {
// this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
const bundleFeatures = { aiErrorAssistant: true }
this.user.features = { aiErrorAssistant: false }
this.Modules.promises.hooks.fire = sinon
.stub()
.resolves([bundleFeatures])
this.res.render = (pageName, opts) => {
expect(opts.user.features).to.deep.equal(bundleFeatures)
this.Modules.promises.hooks.fire.should.have.been.calledWith(
'getModuleProvidedFeatures',
this.user._id
)
done()
}
this.ProjectController.loadEditor(this.req, this.res)
})
it('should handle modules not returning any features', function (done) {
this.Modules.promises.hooks.fire = sinon.stub().resolves([])
this.res.render = (pageName, opts) => {
expect(opts.user.features).to.deep.equal({})
this.Modules.promises.hooks.fire.should.have.been.calledWith(
'getModuleProvidedFeatures',
this.user._id
)
done()
}
this.ProjectController.loadEditor(this.req, this.res)
})
})
})
describe('userProjectsJson', function () {

View file

@ -88,6 +88,34 @@ describe('FeaturesHelper', function () {
})
})
describe('computeFeatureSet', function () {
it('should handle only one featureSet', function () {
expect(
this.FeaturesHelper.computeFeatureSet([
{ github: true, feat1: true, feat2: false },
])
).to.deep.equal({ github: true, feat1: true, feat2: false })
})
it('should handle an empty array of featureSets', function () {
expect(this.FeaturesHelper.computeFeatureSet([])).to.deep.equal({})
})
it('should handle 3+ featureSets', function () {
const featureSets = [
{ github: true, feat1: false, feat2: false },
{ github: false, feat1: true, feat2: false, feat3: false },
{ github: false, feat1: false, feat2: true, feat4: true },
]
expect(this.FeaturesHelper.computeFeatureSet(featureSets)).to.deep.equal({
github: true,
feat1: true,
feat2: true,
feat3: false,
feat4: true,
})
})
})
describe('isFeatureSetBetter', function () {
it('simple comparisons', function () {
const result1 = this.FeaturesHelper.isFeatureSetBetter(