Files
overleaf-cep/services/clsi/test/unit/js/ProjectPersistenceManager.test.js
Jakob Ackermann 81b7121408 [clsi] initial implementation of compile from history (#31883)
* [clsi] initial implementation of compile from history

* [clsi] copy changes

* [saas-e2e] extend test case with nested folder

* [saas-e2e] add test case for tracked changes

* [web] fix accumulating changes from multiple chunks

* [web] optimize size check for compile request payload

* [clsi] deduplicate globalBlobs

* [clsi] add validation for request body details

* [clsi] add metrics for compile from history

* [clsi] download binary files concurrently

* [clsi] skip download of empty file blob

* [clsi] break down e2e compile time metric by compileFromHistory

GitOrigin-RevId: 0dadef93e89d8a172c35cb130a1042d9d1bec42a
2026-03-06 09:12:07 +00:00

198 lines
5.6 KiB
JavaScript

import { vi, describe, beforeEach, it } from 'vitest'
import sinon from 'sinon'
import path from 'node:path'
const modulePath = path.join(
import.meta.dirname,
'../../../app/js/ProjectPersistenceManager'
)
describe('ProjectPersistenceManager', () => {
beforeEach(async ctx => {
ctx.fsPromises = {
statfs: sinon.stub(),
}
vi.doMock('@overleaf/metrics', () => ({
default: (ctx.Metrics = { gauge: sinon.stub() }),
}))
vi.doMock('../../../app/js/UrlCache', () => ({
default: (ctx.UrlCache = {}),
}))
vi.doMock(
'../../../app/js/HistoryResourceWriter',
() =>
(ctx.HistoryResourceWriter = {
clearCacheCb: sinon.stub().yields(null),
})
)
vi.doMock('../../../app/js/CompileManager', () => ({
default: (ctx.CompileManager = {}),
}))
vi.doMock('fs', () => ({
default: { promises: ctx.fsPromises },
}))
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
project_cache_length_ms: 1000,
path: {
compilesDir: '/compiles',
outputDir: '/output',
clsiCacheDir: '/cache',
},
}),
}))
ctx.ProjectPersistenceManager = (await import(modulePath)).default
ctx.callback = sinon.stub()
ctx.project_id = 'project-id-123'
return (ctx.user_id = '1234')
})
describe('refreshExpiryTimeout', () => {
it('should leave expiry alone if plenty of disk', async ctx => {
await new Promise((resolve, reject) => {
ctx.fsPromises.statfs.resolves({
blocks: 100,
bsize: 1,
bavail: 40,
})
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
ctx.Metrics.gauge.should.have.been.calledWith(
'disk_available_percent',
40
)
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(
ctx.settings.project_cache_length_ms
)
resolve()
})
})
})
it('should drop EXPIRY_TIMEOUT 10% if low disk usage', async ctx => {
await new Promise((resolve, reject) => {
ctx.fsPromises.statfs.resolves({
blocks: 100,
bsize: 1,
bavail: 5,
})
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
ctx.Metrics.gauge.should.have.been.calledWith(
'disk_available_percent',
5
)
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900)
resolve()
})
})
})
it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', async ctx => {
await new Promise((resolve, reject) => {
ctx.fsPromises.statfs.resolves({
blocks: 100,
bsize: 1,
bavail: 5,
})
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
ctx.Metrics.gauge.should.have.been.calledWith(
'disk_available_percent',
5
)
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500)
resolve()
})
})
})
it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', async ctx => {
await new Promise((resolve, reject) => {
ctx.fsPromises.statfs.rejects(new Error())
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000)
resolve()
})
})
})
})
describe('clearExpiredProjects', () => {
beforeEach(ctx => {
ctx.project_ids = ['project-id-1', 'project-id-2']
ctx.ProjectPersistenceManager._findExpiredProjectIds = sinon
.stub()
.callsArgWith(0, null, ctx.project_ids)
ctx.ProjectPersistenceManager.clearProjectFromCache = sinon
.stub()
.callsArg(2)
ctx.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1)
return ctx.ProjectPersistenceManager.clearExpiredProjects(ctx.callback)
})
it('should clear each expired project', ctx => {
return Array.from(ctx.project_ids).map(projectId =>
ctx.ProjectPersistenceManager.clearProjectFromCache
.calledWith(projectId)
.should.equal(true)
)
})
return it('should call the callback', ctx => {
return ctx.callback.called.should.equal(true)
})
})
return describe('clearProject', () => {
beforeEach(ctx => {
ctx.ProjectPersistenceManager._clearProjectFromDatabase = sinon
.stub()
.callsArg(1)
ctx.UrlCache.clearProject = sinon.stub().callsArg(2)
ctx.CompileManager.clearProject = sinon.stub().callsArg(2)
return ctx.ProjectPersistenceManager.clearProject(
ctx.project_id,
ctx.user_id,
ctx.callback
)
})
it('should clear the project from the database', ctx => {
return ctx.ProjectPersistenceManager._clearProjectFromDatabase
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should clear the history cache', ctx => {
ctx.HistoryResourceWriter.clearCacheCb.should.have.been.calledWith(
ctx.project_id,
ctx.user_id
)
})
it('should clear all the cached Urls for the project', ctx => {
return ctx.UrlCache.clearProject
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should clear the project compile folder', ctx => {
return ctx.CompileManager.clearProject
.calledWith(ctx.project_id, ctx.user_id)
.should.equal(true)
})
return it('should call the callback', ctx => {
return ctx.callback.called.should.equal(true)
})
})
})