diff --git a/services/history-v1/test/acceptance/js/storage/chunk_buffer.test.js b/services/history-v1/test/acceptance/js/storage/chunk_buffer.test.js index bfd979d0ff..841282a8e4 100644 --- a/services/history-v1/test/acceptance/js/storage/chunk_buffer.test.js +++ b/services/history-v1/test/acceptance/js/storage/chunk_buffer.test.js @@ -8,6 +8,11 @@ const { History, File, AddFileOperation, + EditFileOperation, + AddCommentOperation, + TextOperation, + Range, + TrackingProps, Change, } = require('overleaf-editor-core') const cleanup = require('./support/cleanup') @@ -59,6 +64,9 @@ describe('chunk buffer', function () { }) it('should load from chunk store and update cache on first access (cache miss)', async function () { + // Load the underlying chunk from the chunk store for verification + const storedChunk = await chunkStore.loadLatest(projectId) + // First access should load from chunk store and populate cache const firstResult = await chunkBuffer.loadLatest(projectId) @@ -67,6 +75,9 @@ describe('chunk buffer', function () { expect(firstResult.getStartVersion()).to.equal(1) expect(firstResult.getEndVersion()).to.equal(2) + // Verify the chunk is the same as the one in the store + expect(firstResult).to.deep.equal(storedChunk) + // Verify that we got a cache miss metric expect( metrics.inc.calledWith('chunk_buffer.loadLatest', 1, { @@ -85,6 +96,9 @@ describe('chunk buffer', function () { expect(secondResult.getStartVersion()).to.equal(1) expect(secondResult.getEndVersion()).to.equal(2) + // Verify the chunk is the same as the one in the store + expect(secondResult).to.deep.equal(storedChunk) + // Verify that we got a cache hit metric expect( metrics.inc.calledWith('chunk_buffer.loadLatest', 1, { @@ -99,6 +113,7 @@ describe('chunk buffer', function () { expect(secondResult.getEndVersion()).to.equal( firstResult.getEndVersion() ) + expect(secondResult).to.deep.equal(firstResult) }) it('should refresh the cache when chunk changes in the store', async function () { @@ -129,12 +144,17 @@ describe('chunk buffer', function () { // Store the new chunk directly in the chunk store await chunkStore.create(projectId, newChunk) + // Load the underlying chunk from the chunk store for verification + const storedChunk = await chunkStore.loadLatest(projectId) + // Access again - should detect the change and refresh cache const secondResult = await chunkBuffer.loadLatest(projectId) // Verify we got the updated chunk expect(secondResult.getStartVersion()).to.equal(2) expect(secondResult.getEndVersion()).to.equal(3) + // Verify that the chunk content is the same + expect(secondResult).to.deep.equal(storedChunk) // Verify that we got a cache miss metric (since the cached chunk was invalidated) expect( @@ -145,6 +165,9 @@ describe('chunk buffer', function () { }) it('should continue using cache when chunk in store has not changed', async function () { + // Load the underlying chunk from the chunk store for verification + const storedChunk = await chunkStore.loadLatest(projectId) + // First access to load into cache await chunkBuffer.loadLatest(projectId) @@ -157,6 +180,7 @@ describe('chunk buffer', function () { // Verify we got the same chunk expect(result.getStartVersion()).to.equal(1) expect(result.getEndVersion()).to.equal(2) + expect(result).to.deep.equal(storedChunk) // Verify that we got a cache hit metric expect( @@ -167,11 +191,122 @@ describe('chunk buffer', function () { }) }) + it('should handle a chunk with metadata, comments and tracked changes', async function () { + // Create a snapshot and initial file + const snapshot = new Snapshot() + const initialFileOp = new AddFileOperation( + 'test.tex', + File.fromString('Initial line.\\nSecond line.', { + meta1: 'abc', + meta2: 'def', + }) + ) + const initialChange = new Change([initialFileOp], new Date(), []) + + // Add a comment + const commentOp = new AddCommentOperation( + 'comment1', + [new Range(0, 7)] // Range for "Initial" + ) + const commentChange = new Change( + [new EditFileOperation('test.tex', commentOp)], + new Date(), + [] + ) + + // Tracked insert + const trackedInsertOp = new TextOperation() + .retain(14) + .insert('Hello', { + commentIds: ['comment1'], + tracking: TrackingProps.fromRaw({ + ts: '2024-01-01T00:00:00.000Z', + type: 'insert', + userId: 'user1', + }), + }) + .retain(12) + const insertChange = new Change( + [new EditFileOperation('test.tex', trackedInsertOp)], + new Date(), + [] + ) + + // Tracked delete + const trackedDeleteOp = new TextOperation().retain(14, { + tracking: TrackingProps.fromRaw({ + ts: '2024-01-01T00:00:00.000Z', + type: 'delete', + userId: 'user1', + }), + }) + const deleteChange = new Change( + [new EditFileOperation('test.tex', trackedDeleteOp)], + new Date(), + [] + ) + + // Combine changes into history and create chunk + const history = new History(snapshot, [ + initialChange, + commentChange, + insertChange, + deleteChange, + ]) + const chunk = new Chunk(history, 1) // Start version 0 + // Store the chunk + await chunkStore.create(projectId, chunk) + // Clear the cache + await redisBackend.clearCache(projectId) + metrics.inc.resetHistory() + + // Load the underlying chunk from the chunk store for verification + const storedChunk = await chunkStore.loadLatest(projectId) + + // Load the chunk via buffer (cache miss) + const firstResult = await chunkBuffer.loadLatest(projectId) + + // Verify chunk details + expect(firstResult.getStartVersion()).to.equal(1) + expect(firstResult.getEndVersion()).to.equal(5) // 4 changes + expect(firstResult.history.changes.length).to.equal(4) + expect(firstResult).to.deep.equal(storedChunk) + + // Verify cache miss metric + expect( + metrics.inc.calledWith('chunk_buffer.loadLatest', 1, { + status: 'cache-miss', + }) + ).to.be.true + + // Reset metrics + metrics.inc.resetHistory() + + // Second access should hit the cache + const secondResult = await chunkBuffer.loadLatest(projectId) + + // Verify we got the same chunk + expect(secondResult.getStartVersion()).to.equal(1) + expect(secondResult.getEndVersion()).to.equal(5) + expect(secondResult.history.changes.length).to.equal(4) + expect(secondResult).to.deep.equal(storedChunk) + + // Verify cache hit metric + expect( + metrics.inc.calledWith('chunk_buffer.loadLatest', 1, { + status: 'cache-hit', + }) + ).to.be.true + }) + describe('with an empty project', function () { it('should handle a case with empty chunks (no changes)', async function () { // Clear the cache await redisBackend.clearCache(projectId) + // Load the underlying chunk from the chunk store for verification + const storedChunk = await chunkStore.loadLatest(projectId) + // Load the initial empty chunk via buffer const result = await chunkBuffer.loadLatest(projectId) @@ -180,6 +315,9 @@ describe('chunk buffer', function () { expect(result.getEndVersion()).to.equal(0) // Start equals end for empty chunks expect(result.history.changes.length).to.equal(0) + // Verify that the chunk is the same as the one in the store + expect(result).to.deep.equal(storedChunk) + // Verify cache miss metric expect( metrics.inc.calledWith('chunk_buffer.loadLatest', 1, { @@ -198,6 +336,9 @@ describe('chunk buffer', function () { expect(secondResult.getEndVersion()).to.equal(0) expect(secondResult.history.changes.length).to.equal(0) + // Verify that the chunk is the same as the one in the store + expect(secondResult).to.deep.equal(storedChunk) + // Verify cache hit metric expect( metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {