[web] fetch file-tree in fallback to old compile mode (#32403)

* [web] fetch file-tree in fallback to old compile mode

* [web] tweak unit test

GitOrigin-RevId: 1c5f8cb1f0cb15c91efc46babec343d228631019
This commit is contained in:
Jakob Ackermann
2026-03-24 08:58:59 +01:00
committed by Copybot
parent adef5b26fb
commit 205573c01a
2 changed files with 192 additions and 8 deletions

View File

@@ -768,6 +768,10 @@ async function _buildRequest(projectId, userId, options) {
'failed to compose history-full request'
)
// fall back to old compile mode
return await _buildRequest(projectId, userId, {
...options,
compileFromHistory: false,
})
}
} else if (options.compileFromHistory) {
// incremental sync
@@ -785,6 +789,10 @@ async function _buildRequest(projectId, userId, options) {
'failed to compose history-incremental request'
)
// fall back to old compile mode
return await _buildRequest(projectId, userId, {
...options,
compileFromHistory: false,
})
}
}

View File

@@ -3,6 +3,7 @@ import { setTimeout } from 'node:timers/promises'
import sinon from 'sinon'
import tk from 'timekeeper'
import { RequestFailedError } from '@overleaf/fetch-utils'
import _ from 'lodash'
const FILESTORE_URL = 'http://filestore.example.com'
const CLSI_HOST = 'clsi.example.com'
@@ -15,13 +16,6 @@ describe('ClsiManager', function () {
tk.freeze(Date.now())
ctx.user_id = 'user-id'
ctx.project = {
_id: 'project-id',
compiler: 'latex',
rootDoc_id: 'mock-doc-id-1',
imageName: 'mock-image-name',
overleaf: { history: { id: 42 } },
}
ctx.docs = {
'/main.tex': {
name: 'main.tex',
@@ -53,6 +47,20 @@ describe('ClsiManager', function () {
created: new Date(),
},
}
ctx.project = {
_id: 'project-id',
compiler: 'latex',
rootDoc_id: 'mock-doc-id-1',
imageName: 'mock-image-name',
overleaf: { history: { id: 42 } },
rootFolder: [
{
docs: [],
files: [],
folders: [],
},
],
}
ctx.clsiCookieKey = 'clsiserver'
ctx.clsiServerId = 'clsi-server-id'
ctx.newClsiServerId = 'newserver'
@@ -121,7 +129,17 @@ describe('ClsiManager', function () {
ctx.ProjectGetter = {
promises: {
findById: sinon.stub().resolves(ctx.project),
getProject: sinon.stub().resolves(ctx.project),
getProject: sinon.stub().callsFake((projectId, projection) => {
const result = { _id: ctx.project._id }
for (const [field, v] of Object.entries(projection)) {
if (v) {
_.set(result, field, _.get(ctx.project, field))
} else {
_.unset(result, field)
}
}
return result
}),
},
}
ctx.DocumentUpdaterHandler = {
@@ -162,6 +180,9 @@ describe('ClsiManager', function () {
clearCache: sinon.stub().resolves(),
}
ctx.HistoryManager = {
promises: {
flushProject: sinon.stub().resolves(),
},
getFilestoreBlobURL: sinon.stub().callsFake((historyId, hash) => {
if (hash === GLOBAL_BLOB_HASH) {
return `${FILESTORE_URL}/history/global/hash/${hash}`
@@ -392,6 +413,155 @@ describe('ClsiManager', function () {
})
})
describe('with compile from history fallback to incremental', function () {
const buildId = '18fbe9e7564-30dcb2f71250c690'
beforeEach(async function (ctx) {
ctx.outputFiles = [
{
url: `/project/${ctx.project_id}/user/${ctx.user_id}/build/1234/output/output.pdf`,
path: 'output.pdf',
type: 'pdf',
build: buildId,
},
{
url: `/project/${ctx.project_id}/user/${ctx.user_id}/build/1234/output/output.log`,
path: 'output.log',
type: 'log',
build: buildId,
},
]
ctx.responseBody.compile.outputFiles = ctx.outputFiles.map(
outputFile => ({
...outputFile,
url: `http://${CLSI_HOST}${outputFile.url}`,
})
)
ctx.responseBody.compile.buildId = buildId
ctx.timeout = 100
ctx.HistoryManager.promises.flushProject.rejects()
const doc = ctx.docs['/main.tex']
ctx.DocumentUpdaterHandler.promises.getProjectDocsIfMatch.resolves([
{ _id: doc._id, lines: doc.lines, v: 123 },
])
ctx.ProjectEntityHandler.getAllDocPathsFromProject.returns({
'mock-doc-id-1': 'main.tex',
})
ctx.result = await ctx.ClsiManager.promises.sendRequest(
ctx.project._id,
ctx.user_id,
{
compileBackendClass: 'c3d',
compileGroup: 'standard',
timeout: ctx.timeout,
compileFromHistory: true,
incrementalCompilesEnabled: true,
}
)
})
it('should send the request to the CLSI', function (ctx) {
const doc = ctx.docs['/main.tex']
ctx.FetchUtils.fetchStringWithResponse.should.have.been.calledWith(
sinon.match(
url =>
url.host === CLSI_HOST &&
url.pathname ===
`/project/${ctx.project._id}/user/${ctx.user_id}/compile` &&
url.searchParams.get('compileBackendClass') === 'c3d' &&
url.searchParams.get('compileGroup') === 'standard'
),
{
method: 'POST',
json: sinon.match({
compile: {
options: {
compiler: ctx.project.compiler,
imageName: ctx.project.imageName,
timeout: ctx.timeout,
draft: false,
compileGroup: 'standard',
metricsMethod: 'standard',
stopOnFirstError: false,
syncType: 'incremental',
syncState: '01234567890abcdef',
},
rootResourcePath: 'main.tex',
resources: [
{ path: 'main.tex', content: doc.lines.join('\n') },
],
},
}),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Cookie: `${ctx.clsiCookieKey}=${ctx.clsiServerId}`,
},
signal: sinon.match.instanceOf(AbortSignal),
}
)
})
it('should compute the hash with the full project', function (ctx) {
ctx.ClsiStateManager.computeHash.should.have.been.calledWithMatch({
rootFolder: ctx.project.rootFolder,
})
})
it('should get the project with the required fields', function (ctx) {
ctx.ProjectGetter.promises.getProject.should.have.been.calledWith(
ctx.project._id,
{
compiler: 1,
rootDoc_id: 1,
imageName: 1,
rootFolder: 1,
'overleaf.history.id': 1,
}
)
})
it('should get only the live docs from the docupdater with a background flush in docupdater', function (ctx) {
ctx.DocumentUpdaterHandler.promises.getProjectDocsIfMatch.should.have.been.calledWith(
ctx.project._id
)
})
it('should not get any docs from mongo', function (ctx) {
ctx.ProjectEntityHandler.promises.getAllDocs.should.not.have.been.calledWith(
ctx.project._id
)
})
it('should not get any of the files', function (ctx) {
ctx.ProjectEntityHandler.promises.getAllFiles.should.not.have.been
.called
})
it('should return the status and output files', function (ctx) {
expect(ctx.result.status).to.equal('success')
expect(ctx.result.outputFiles.map(f => f.path)).to.have.members(
ctx.outputFiles.map(f => f.path)
)
})
it('should return the buildId', function (ctx) {
expect(ctx.result.buildId).to.equal(buildId)
})
it('should persist the cookie from the response', function (ctx) {
expect(
ctx.ClsiCookieManager.promises.setServerId
).to.have.been.calledWith(
ctx.project._id,
ctx.user_id,
'standard',
'c3d',
ctx.newClsiServerId
)
})
})
describe('with ranges on the pdf and stats/timings details', function () {
beforeEach(async function (ctx) {
ctx.ranges = [{ start: 1, end: 42, hash: 'foo' }]
@@ -499,6 +669,12 @@ describe('ClsiManager', function () {
)
})
it('should not get any docs from mongo', function (ctx) {
ctx.ProjectEntityHandler.promises.getAllDocs.should.not.have.been.calledWith(
ctx.project._id
)
})
it('should not get any of the files', function (ctx) {
ctx.ProjectEntityHandler.promises.getAllFiles.should.not.have.been
.called