[web] let docstore determine a projects comment thread ids (#26364)

* [docstore] add endpoint for getting a projects comment thread ids

* [web] let docstore determine a projects comment thread ids

Also fetch the comment thread ids once when reverting project.

GitOrigin-RevId: c3ebab976821509c9627962e58918f9c6ebb0e1d
This commit is contained in:
Jakob Ackermann
2025-06-13 08:08:00 +00:00
committed by Copybot
parent e95b159edd
commit f025f1d0cb
8 changed files with 278 additions and 42 deletions
@@ -9,6 +9,12 @@ const tk = require('timekeeper')
const moment = require('moment')
const { expect } = require('chai')
function nestedMapWithSetToObject(m) {
return Object.fromEntries(
Array.from(m.entries()).map(([key, set]) => [key, Array.from(set)])
)
}
describe('RestoreManager', function () {
beforeEach(function () {
tk.freeze(Date.now()) // freeze the time for these tests
@@ -28,7 +34,7 @@ describe('RestoreManager', function () {
promises: { flushProjectToMongo: sinon.stub().resolves() },
}),
'../Docstore/DocstoreManager': (this.DocstoreManager = {
promises: {},
promises: { getCommentThreadIds: sinon.stub().resolves({}) },
}),
'../Chat/ChatApiHandler': (this.ChatApiHandler = { promises: {} }),
'../Chat/ChatManager': (this.ChatManager = { promises: {} }),
@@ -269,13 +275,9 @@ describe('RestoreManager', function () {
{ op: { t: 'single-comment', p: 10, c: 'bar' } },
]
this.ProjectLocator.promises.findElementByPath = sinon.stub().rejects()
this.DocstoreManager.promises.getAllRanges = sinon.stub().resolves([
{
ranges: {
comments: this.comments.slice(0, 1),
},
},
])
this.DocstoreManager.promises.getCommentThreadIds = sinon
.stub()
.resolves({ 'other-doc': [this.comments[0].op.t] })
this.ChatApiHandler.promises.duplicateCommentThreads = sinon
.stub()
.resolves({
@@ -355,7 +357,7 @@ describe('RestoreManager', function () {
expect(
this.DocumentUpdaterHandler.promises.flushProjectToMongo
).to.have.been.calledBefore(
this.DocstoreManager.promises.getAllRanges
this.DocstoreManager.promises.getCommentThreadIds
)
})
@@ -451,19 +453,11 @@ describe('RestoreManager', function () {
)
})
it('should delete the document before flushing', function () {
expect(
this.EditorController.promises.deleteEntity
).to.have.been.calledBefore(
this.DocumentUpdaterHandler.promises.flushProjectToMongo
)
})
it('should flush the document before fetching ranges', function () {
expect(
this.DocumentUpdaterHandler.promises.flushProjectToMongo
).to.have.been.calledBefore(
this.DocstoreManager.promises.getAllRanges
this.DocstoreManager.promises.getCommentThreadIds
)
})
@@ -499,6 +493,143 @@ describe('RestoreManager', function () {
)
})
})
describe('with comments in same doc', function () {
// copy of the above, addition: inject and later inspect threadIds set
beforeEach(async function () {
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
this.EditorController.promises.deleteEntity = sinon.stub().resolves()
this.ChatApiHandler.promises.generateThreadData = sinon
.stub()
.resolves(
(this.threadData = {
[this.comments[0].op.t]: {
messages: [
{
content: 'message',
timestamp: '2024-01-01T00:00:00.000Z',
user_id: 'user-1',
},
],
},
[this.comments[1].op.t]: {
messages: [
{
content: 'other message',
timestamp: '2024-01-01T00:00:00.000Z',
user_id: 'user-1',
},
],
},
})
)
this.threadIds = new Map([
[
'mock-file-id',
new Set([this.comments[0].op.t, this.comments[1].op.t]),
],
])
// Comments are updated in-place. Look up threads before reverting.
this.afterThreadIds = {
// mock-file-id removed
[this.addedFile._id]: [
this.comments[0].op.t,
this.comments[1].op.t,
],
}
this.data = await this.RestoreManager.promises._revertSingleFile(
this.user_id,
this.project_id,
this.version,
this.pathname,
this.threadIds
)
})
it('should import the file with original comments minus the deleted one', function () {
expect(
this.EditorController.promises.addDocWithRanges
).to.have.been.calledWith(
this.project_id,
this.folder_id,
'foo.tex',
['foo', 'bar', 'baz'],
{
changes: this.tracked_changes,
comments: this.comments.slice(0, 2),
},
{
kind: 'file-restore',
path: this.pathname,
version: this.version,
timestamp: new Date(this.endTs).toISOString(),
}
)
})
it('should add the seen thread ids to the map', function () {
expect(nestedMapWithSetToObject(this.threadIds)).to.deep.equal(
this.afterThreadIds
)
})
})
describe('with remapped comments during revertProject', function () {
// copy of the above, addition: inject and later inspect threadIds set
beforeEach(async function () {
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
this.EditorController.promises.deleteEntity = sinon.stub().resolves()
this.threadIds = new Map([
['other-doc', new Set([this.comments[0].op.t])],
])
// Comments are updated in-place. Look up threads before reverting.
this.afterThreadIds = {
// mock-file-id removed
'other-doc': [this.comments[0].op.t],
[this.addedFile._id]: [
this.remappedComments[0].op.t,
this.remappedComments[1].op.t,
],
}
this.data = await this.RestoreManager.promises._revertSingleFile(
this.user_id,
this.project_id,
this.version,
this.pathname,
this.threadIds
)
})
it('should import the file', function () {
expect(
this.EditorController.promises.addDocWithRanges
).to.have.been.calledWith(
this.project_id,
this.folder_id,
'foo.tex',
['foo', 'bar', 'baz'],
{ changes: this.tracked_changes, comments: this.remappedComments },
{
kind: 'file-restore',
path: this.pathname,
version: this.version,
timestamp: new Date(this.endTs).toISOString(),
}
)
})
it('should add the seen thread ids to the map', function () {
expect(nestedMapWithSetToObject(this.threadIds)).to.deep.equal(
this.afterThreadIds
)
})
})
})
describe('reverting a file or document with metadata', function () {
@@ -524,7 +655,9 @@ describe('RestoreManager', function () {
.stub()
.resolves((this.addedFile = { _id: 'mock-doc-id', type: 'doc' }))
this.DocstoreManager.promises.getAllRanges = sinon.stub().resolves([])
this.DocstoreManager.promises.getCommentThreadIds = sinon
.stub()
.resolves({})
this.ChatApiHandler.promises.generateThreadData = sinon
.stub()
.resolves({})
@@ -741,7 +874,7 @@ describe('RestoreManager', function () {
this.ProjectGetter.promises.getProject
.withArgs(this.project_id)
.resolves({ overleaf: { history: { rangesSupportEnabled: true } } })
this.RestoreManager.promises.revertFile = sinon.stub().resolves()
this.RestoreManager.promises._revertSingleFile = sinon.stub().resolves()
this.RestoreManager.promises._getProjectPathsAtVersion = sinon
.stub()
.resolves([])
@@ -832,21 +965,27 @@ describe('RestoreManager', function () {
})
it('should revert the old files', function () {
expect(this.RestoreManager.promises.revertFile).to.have.been.calledWith(
expect(
this.RestoreManager.promises._revertSingleFile
).to.have.been.calledWith(
this.user_id,
this.project_id,
this.version,
'main.tex'
)
expect(this.RestoreManager.promises.revertFile).to.have.been.calledWith(
expect(
this.RestoreManager.promises._revertSingleFile
).to.have.been.calledWith(
this.user_id,
this.project_id,
this.version,
'figures/image.png'
)
expect(this.RestoreManager.promises.revertFile).to.have.been.calledWith(
expect(
this.RestoreManager.promises._revertSingleFile
).to.have.been.calledWith(
this.user_id,
this.project_id,
this.version,
@@ -856,7 +995,7 @@ describe('RestoreManager', function () {
it('should not revert the current files', function () {
expect(
this.RestoreManager.promises.revertFile
this.RestoreManager.promises._revertSingleFile
).to.not.have.been.calledWith(
this.user_id,
this.project_id,