ChunkTranslatorTests.js 93 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142
  1. import sinon from 'sinon'
  2. import { expect } from 'chai'
  3. import { strict as esmock } from 'esmock'
  4. const MODULE_PATH = '../../../../app/js/ChunkTranslator.js'
  5. describe('ChunkTranslator', function () {
  6. beforeEach(async function () {
  7. this.projectId = '0123456789abc0123456789abc'
  8. this.historyId = 12345
  9. this.author1 = {
  10. id: 1,
  11. email: 'james.allen@overleaf.com',
  12. name: 'James Allen',
  13. }
  14. this.date = new Date()
  15. this.fileHash = 'some_hash'
  16. this.fileContents = 'Hello world, this is a test'
  17. this.HistoryStoreManager = {
  18. getProjectBlob: sinon.stub(),
  19. }
  20. this.HistoryStoreManager.getProjectBlob
  21. .withArgs(this.historyId, this.fileHash)
  22. .yields(null, this.fileContents)
  23. this.WebApiManager = {
  24. getHistoryId: sinon.stub().callsFake((projectId, cb) => {
  25. console.log({ projectId })
  26. }),
  27. }
  28. this.WebApiManager.getHistoryId
  29. .withArgs(this.projectId)
  30. .yields(null, this.historyId)
  31. this.ChunkTranslator = await esmock(MODULE_PATH, {
  32. '../../../../app/js/HistoryStoreManager.js': this.HistoryStoreManager,
  33. '../../../../app/js/WebApiManager.js': this.WebApiManager,
  34. })
  35. this.callback = sinon.stub()
  36. })
  37. describe('with changes to the text', function () {
  38. beforeEach(function () {
  39. this.chunk = {
  40. project_id: this.projectId,
  41. chunk: {
  42. startVersion: 0,
  43. history: {
  44. snapshot: {
  45. files: {
  46. 'main.tex': {
  47. hash: this.fileHash,
  48. stringLength: 42,
  49. },
  50. },
  51. },
  52. changes: [
  53. {
  54. operations: [
  55. { pathname: 'main.tex', textOperation: ['Hello test, ', -6] },
  56. ],
  57. timestamp: this.date.toISOString(),
  58. authors: [this.author1.id],
  59. },
  60. {
  61. operations: [
  62. { pathname: 'main.tex', textOperation: [6, 'foo '] },
  63. ],
  64. timestamp: this.date.toISOString(),
  65. authors: [this.author1.id],
  66. },
  67. {
  68. operations: [{ pathname: 'main.tex', textOperation: [6, -4] }],
  69. timestamp: this.date.toISOString(),
  70. authors: [this.author1.id],
  71. },
  72. {
  73. origin: {
  74. kind: 'file-restore',
  75. version: 1,
  76. path: 'main.tex',
  77. timestamp: this.date.toISOString(),
  78. },
  79. operations: [
  80. {
  81. pathname: 'main.tex',
  82. newPathname: '',
  83. },
  84. ],
  85. timestamp: this.date.toISOString(),
  86. authors: [this.author1.id],
  87. },
  88. {
  89. origin: {
  90. kind: 'file-restore',
  91. version: 1,
  92. path: 'main.tex',
  93. timestamp: this.date.toISOString(),
  94. },
  95. operations: [
  96. {
  97. pathname: 'main.tex',
  98. file: {
  99. hash: this.fileHash,
  100. stringLength: 42,
  101. },
  102. },
  103. ],
  104. timestamp: this.date.toISOString(),
  105. authors: [this.author1.id],
  106. },
  107. {
  108. operations: [
  109. {
  110. pathname: 'main.tex',
  111. newPathname: 'main2.tex',
  112. },
  113. ],
  114. timestamp: this.date.toISOString(),
  115. authors: [this.author1.id],
  116. },
  117. ],
  118. },
  119. },
  120. authors: [this.author1.id],
  121. }
  122. })
  123. describe('convertToDiffUpdates', function () {
  124. it('should convert them to insert and delete ops', function (done) {
  125. this.ChunkTranslator.convertToDiffUpdates(
  126. this.projectId,
  127. this.chunk,
  128. 'main.tex',
  129. 0,
  130. 3,
  131. (error, param) => {
  132. if (param == null) {
  133. param = {}
  134. }
  135. const { initialContent, updates } = param
  136. expect(error).to.be.null
  137. expect(initialContent).to.equal(this.fileContents)
  138. expect(updates).to.deep.equal([
  139. {
  140. op: [
  141. { i: 'Hello test, ', p: 0 },
  142. { d: 'Hello ', p: 12 },
  143. ],
  144. meta: {
  145. users: [this.author1.id],
  146. start_ts: this.date.getTime(),
  147. end_ts: this.date.getTime(),
  148. },
  149. v: 0,
  150. },
  151. {
  152. op: [{ i: 'foo ', p: 6 }],
  153. meta: {
  154. users: [this.author1.id],
  155. start_ts: this.date.getTime(),
  156. end_ts: this.date.getTime(),
  157. },
  158. v: 1,
  159. },
  160. {
  161. op: [{ d: 'foo ', p: 6 }],
  162. meta: {
  163. users: [this.author1.id],
  164. start_ts: this.date.getTime(),
  165. end_ts: this.date.getTime(),
  166. },
  167. v: 2,
  168. },
  169. ])
  170. done()
  171. }
  172. )
  173. })
  174. it('should return the correct initial text if there are previous changes', function (done) {
  175. this.ChunkTranslator.convertToDiffUpdates(
  176. this.projectId,
  177. this.chunk,
  178. 'main.tex',
  179. 2,
  180. 3,
  181. (error, param) => {
  182. if (param == null) {
  183. param = {}
  184. }
  185. const { initialContent, updates } = param
  186. expect(error).to.be.null
  187. expect(initialContent).to.equal(
  188. 'Hello foo test, world, this is a test'
  189. )
  190. expect(updates).to.deep.equal([
  191. {
  192. op: [{ d: 'foo ', p: 6 }],
  193. meta: {
  194. users: [this.author1.id],
  195. start_ts: this.date.getTime(),
  196. end_ts: this.date.getTime(),
  197. },
  198. v: 2,
  199. },
  200. ])
  201. done()
  202. }
  203. )
  204. })
  205. it('should return the correct initial text in case of file restore', function (done) {
  206. this.ChunkTranslator.convertToDiffUpdates(
  207. this.projectId,
  208. this.chunk,
  209. 'main.tex',
  210. 3,
  211. 5,
  212. (error, param) => {
  213. const { initialContent } = param
  214. expect(error).to.be.null
  215. expect(initialContent).to.equal('Hello world, this is a test')
  216. done()
  217. }
  218. )
  219. })
  220. it('should still find original file in case it was renamed', function (done) {
  221. this.ChunkTranslator.convertToDiffUpdates(
  222. this.projectId,
  223. this.chunk,
  224. 'main.tex',
  225. 5,
  226. 6,
  227. (error, param) => {
  228. const { initialContent } = param
  229. expect(error).to.be.null
  230. expect(initialContent).to.equal('Hello world, this is a test')
  231. done()
  232. }
  233. )
  234. })
  235. })
  236. describe('convertToSummarizedUpdates', function () {
  237. it('should return a summary of which docs changes when', function (done) {
  238. const assertion = (error, updates) => {
  239. expect(error).to.be.null
  240. expect(updates).to.deep.equal([
  241. {
  242. pathnames: ['main.tex'],
  243. project_ops: [],
  244. meta: {
  245. users: [this.author1.id],
  246. start_ts: this.date.getTime(),
  247. end_ts: this.date.getTime(),
  248. },
  249. v: 0,
  250. },
  251. {
  252. pathnames: ['main.tex'],
  253. project_ops: [],
  254. meta: {
  255. users: [this.author1.id],
  256. start_ts: this.date.getTime(),
  257. end_ts: this.date.getTime(),
  258. },
  259. v: 1,
  260. },
  261. {
  262. pathnames: ['main.tex'],
  263. project_ops: [],
  264. meta: {
  265. users: [this.author1.id],
  266. start_ts: this.date.getTime(),
  267. end_ts: this.date.getTime(),
  268. },
  269. v: 2,
  270. },
  271. {
  272. pathnames: [],
  273. project_ops: [
  274. {
  275. remove: {
  276. pathname: 'main.tex',
  277. },
  278. },
  279. ],
  280. meta: {
  281. users: [this.author1.id],
  282. start_ts: this.date.getTime(),
  283. end_ts: this.date.getTime(),
  284. origin: {
  285. kind: 'file-restore',
  286. version: 1,
  287. path: 'main.tex',
  288. timestamp: this.date.toISOString(),
  289. },
  290. },
  291. v: 3,
  292. },
  293. {
  294. pathnames: [],
  295. project_ops: [
  296. {
  297. add: {
  298. pathname: 'main.tex',
  299. },
  300. },
  301. ],
  302. meta: {
  303. users: [this.author1.id],
  304. start_ts: this.date.getTime(),
  305. end_ts: this.date.getTime(),
  306. origin: {
  307. kind: 'file-restore',
  308. version: 1,
  309. path: 'main.tex',
  310. timestamp: this.date.toISOString(),
  311. },
  312. },
  313. v: 4,
  314. },
  315. {
  316. pathnames: [],
  317. project_ops: [
  318. {
  319. rename: {
  320. pathname: 'main.tex',
  321. newPathname: 'main2.tex',
  322. },
  323. },
  324. ],
  325. meta: {
  326. users: [this.author1.id],
  327. start_ts: this.date.getTime(),
  328. end_ts: this.date.getTime(),
  329. },
  330. v: 5,
  331. },
  332. ])
  333. done()
  334. }
  335. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  336. })
  337. })
  338. })
  339. describe('with a sequence of inserts and deletes', function () {
  340. beforeEach(function () {
  341. this.fileHash = 'some_other_hash'
  342. this.initialFileContents = 'aa bbbbb ccc '
  343. this.HistoryStoreManager.getProjectBlob
  344. .withArgs(this.historyId, this.fileHash)
  345. .yields(null, this.initialFileContents)
  346. this.chunk = {
  347. chunk: {
  348. startVersion: 0,
  349. history: {
  350. snapshot: {
  351. files: {
  352. 'main.tex': {
  353. hash: this.fileHash,
  354. stringLength: 42,
  355. },
  356. },
  357. },
  358. changes: [
  359. {
  360. operations: [
  361. {
  362. pathname: 'main.tex',
  363. textOperation: [
  364. '111 ', // -> "111 aa bbbbb ccc "
  365. -3, // -> "111 bbbbb ccc "
  366. 6, // -> "111 bbbbb ccc "
  367. '2222 ', // -> "111 bbbbb 2222 ccc "
  368. -1, // -> "111 bbbbb 2222 cc "
  369. 'd', // -> "111 bbbbb 2222 dcc "
  370. 3,
  371. ],
  372. },
  373. ],
  374. timestamp: this.date.toISOString(),
  375. authors: [this.author1.id],
  376. },
  377. ],
  378. },
  379. },
  380. authors: [this.author1.id],
  381. }
  382. })
  383. describe('convertToDiffUpdates', function () {
  384. it('should convert them to insert and delete ops', function (done) {
  385. this.ChunkTranslator.convertToDiffUpdates(
  386. this.projectId,
  387. this.chunk,
  388. 'main.tex',
  389. 0,
  390. 1,
  391. (error, param) => {
  392. if (param == null) {
  393. param = {}
  394. }
  395. const { initialContent, updates } = param
  396. expect(error).to.be.null
  397. expect(initialContent).to.equal(this.initialFileContents)
  398. expect(updates).to.deep.equal([
  399. {
  400. op: [
  401. { i: '111 ', p: 0 },
  402. { d: 'aa ', p: 4 },
  403. // NOTE: The construction of TextOperation can merge an
  404. // insertion across a deletion operation, which is why this is
  405. // ever so slightly different from the textOperation defined
  406. // in the chunk. Both diffs represent the same change in
  407. // content.
  408. { i: '2222 d', p: 10 },
  409. { d: 'c', p: 16 },
  410. ],
  411. meta: {
  412. users: [this.author1.id],
  413. start_ts: this.date.getTime(),
  414. end_ts: this.date.getTime(),
  415. },
  416. v: 0,
  417. },
  418. ])
  419. done()
  420. }
  421. )
  422. })
  423. it('should apply them to the text correctly', function (done) {
  424. this.ChunkTranslator.convertToDiffUpdates(
  425. this.projectId,
  426. this.chunk,
  427. 'main.tex',
  428. 1,
  429. 1,
  430. (error, param) => {
  431. if (param == null) {
  432. param = {}
  433. }
  434. const { initialContent, updates } = param
  435. expect(error).to.be.null
  436. expect(initialContent).to.equal('111 bbbbb 2222 dcc ')
  437. expect(updates).to.deep.equal([])
  438. done()
  439. }
  440. )
  441. })
  442. })
  443. })
  444. describe('with unknown operations', function (done) {
  445. beforeEach(function () {
  446. this.chunk = {
  447. chunk: {
  448. startVersion: 0,
  449. history: {
  450. snapshot: {
  451. files: {
  452. 'main.tex': {
  453. hash: this.fileHash,
  454. stringLength: 42,
  455. },
  456. },
  457. },
  458. changes: [
  459. {
  460. operations: [{ unknown: true }],
  461. timestamp: this.date.toISOString(),
  462. authors: [this.author1.id, undefined],
  463. },
  464. {
  465. operations: [
  466. { pathname: 'main.tex', textOperation: [3, 'Hello world'] },
  467. ],
  468. timestamp: this.date.toISOString(),
  469. authors: [this.author1.id, undefined],
  470. },
  471. ],
  472. },
  473. },
  474. authors: [this.author1.id],
  475. }
  476. })
  477. describe('convertToDiffUpdates', function () {
  478. it('should ignore the unknown update', function (done) {
  479. this.ChunkTranslator.convertToDiffUpdates(
  480. this.projectId,
  481. this.chunk,
  482. 'main.tex',
  483. 0,
  484. 2,
  485. (error, param) => {
  486. if (param == null) {
  487. param = {}
  488. }
  489. const { initialContent, updates } = param
  490. expect(error).to.be.null
  491. expect(initialContent).to.equal(this.fileContents)
  492. expect(updates).to.deep.equal([
  493. {
  494. op: [{ i: 'Hello world', p: 3 }],
  495. meta: {
  496. users: [this.author1.id, null],
  497. start_ts: this.date.getTime(),
  498. end_ts: this.date.getTime(),
  499. },
  500. v: 1,
  501. },
  502. ])
  503. done()
  504. }
  505. )
  506. })
  507. })
  508. describe('convertToSummarizedUpdates', function () {
  509. it('should ignore the unknown update', function (done) {
  510. const assertion = (error, updates) => {
  511. expect(error).to.be.null
  512. expect(updates).to.deep.equal([
  513. {
  514. pathnames: [],
  515. project_ops: [],
  516. meta: {
  517. users: [this.author1.id, null],
  518. start_ts: this.date.getTime(),
  519. end_ts: this.date.getTime(),
  520. },
  521. v: 0,
  522. },
  523. {
  524. pathnames: ['main.tex'],
  525. project_ops: [],
  526. meta: {
  527. users: [this.author1.id, null],
  528. start_ts: this.date.getTime(),
  529. end_ts: this.date.getTime(),
  530. },
  531. v: 1,
  532. },
  533. ])
  534. done()
  535. }
  536. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  537. })
  538. })
  539. })
  540. describe('with changes to multiple files', function (done) {
  541. beforeEach(function () {
  542. this.chunk = {
  543. chunk: {
  544. startVersion: 0,
  545. history: {
  546. snapshot: {
  547. files: {
  548. 'main.tex': {
  549. hash: this.fileHash,
  550. stringLength: 42,
  551. },
  552. 'other.tex': {
  553. hash: this.fileHash,
  554. stringLength: 42,
  555. },
  556. },
  557. },
  558. changes: [
  559. {
  560. operations: [
  561. { pathname: 'other.tex', textOperation: [0, 'foo'] },
  562. ],
  563. timestamp: this.date.toISOString(),
  564. authors: [this.author1.id],
  565. },
  566. {
  567. operations: [
  568. { pathname: 'main.tex', textOperation: [6, 'bar'] },
  569. ],
  570. timestamp: this.date.toISOString(),
  571. authors: [this.author1.id],
  572. },
  573. {
  574. operations: [
  575. { pathname: 'other.tex', textOperation: [9, 'baz'] },
  576. ],
  577. timestamp: this.date.toISOString(),
  578. authors: [this.author1.id],
  579. },
  580. {
  581. operations: [
  582. { pathname: 'main.tex', textOperation: [12, 'qux'] },
  583. ],
  584. timestamp: this.date.toISOString(),
  585. authors: [this.author1.id],
  586. },
  587. ],
  588. },
  589. },
  590. authors: [this.author1.id],
  591. }
  592. })
  593. describe('convertToDiffUpdates', function () {
  594. it('should only return the changes to the requested file', function (done) {
  595. this.ChunkTranslator.convertToDiffUpdates(
  596. this.projectId,
  597. this.chunk,
  598. 'main.tex',
  599. 0,
  600. 4,
  601. (error, param) => {
  602. if (param == null) {
  603. param = {}
  604. }
  605. const { initialContent, updates } = param
  606. expect(error).to.be.null
  607. expect(initialContent).to.equal('Hello world, this is a test')
  608. expect(updates).to.deep.equal([
  609. {
  610. op: [{ i: 'bar', p: 6 }],
  611. meta: {
  612. users: [this.author1.id],
  613. start_ts: this.date.getTime(),
  614. end_ts: this.date.getTime(),
  615. },
  616. v: 1,
  617. },
  618. {
  619. op: [{ i: 'qux', p: 12 }],
  620. meta: {
  621. users: [this.author1.id],
  622. start_ts: this.date.getTime(),
  623. end_ts: this.date.getTime(),
  624. },
  625. v: 3,
  626. },
  627. ])
  628. done()
  629. }
  630. )
  631. })
  632. })
  633. describe('convertToSummarizedUpdates', function () {
  634. it('should return a summary of which docs changes when', function (done) {
  635. const assertion = (error, updates) => {
  636. expect(error).to.be.null
  637. expect(updates).to.deep.equal([
  638. {
  639. pathnames: ['other.tex'],
  640. project_ops: [],
  641. meta: {
  642. users: [this.author1.id],
  643. start_ts: this.date.getTime(),
  644. end_ts: this.date.getTime(),
  645. },
  646. v: 0,
  647. },
  648. {
  649. pathnames: ['main.tex'],
  650. project_ops: [],
  651. meta: {
  652. users: [this.author1.id],
  653. start_ts: this.date.getTime(),
  654. end_ts: this.date.getTime(),
  655. },
  656. v: 1,
  657. },
  658. {
  659. pathnames: ['other.tex'],
  660. project_ops: [],
  661. meta: {
  662. users: [this.author1.id],
  663. start_ts: this.date.getTime(),
  664. end_ts: this.date.getTime(),
  665. },
  666. v: 2,
  667. },
  668. {
  669. pathnames: ['main.tex'],
  670. project_ops: [],
  671. meta: {
  672. users: [this.author1.id],
  673. start_ts: this.date.getTime(),
  674. end_ts: this.date.getTime(),
  675. },
  676. v: 3,
  677. },
  678. ])
  679. done()
  680. }
  681. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  682. })
  683. })
  684. })
  685. describe('when the file is created during the chunk', function () {
  686. beforeEach(function () {
  687. this.chunk = {
  688. chunk: {
  689. startVersion: 0,
  690. history: {
  691. snapshot: {
  692. files: {
  693. 'main.tex': {
  694. hash: this.fileHash,
  695. stringLength: 42,
  696. },
  697. },
  698. },
  699. changes: [
  700. {
  701. operations: [
  702. { pathname: 'main.tex', textOperation: [6, 'bar'] },
  703. ],
  704. timestamp: this.date.toISOString(),
  705. authors: [this.author1.id],
  706. },
  707. {
  708. operations: [
  709. {
  710. pathname: 'new.tex',
  711. file: { hash: this.fileHash, stringLength: 10 },
  712. },
  713. ],
  714. timestamp: this.date.toISOString(),
  715. authors: [this.author1.id],
  716. },
  717. {
  718. operations: [
  719. { pathname: 'new.tex', textOperation: [6, 'bar'] },
  720. ],
  721. timestamp: this.date.toISOString(),
  722. authors: [this.author1.id],
  723. },
  724. {
  725. operations: [
  726. { pathname: 'new.tex', textOperation: [9, 'baz'] },
  727. ],
  728. timestamp: this.date.toISOString(),
  729. authors: [this.author1.id],
  730. },
  731. ],
  732. },
  733. },
  734. authors: [this.author1],
  735. }
  736. })
  737. describe('convertToDiffUpdates', function () {
  738. it('returns changes after the file was created before the fromVersion', function (done) {
  739. this.ChunkTranslator.convertToDiffUpdates(
  740. this.projectId,
  741. this.chunk,
  742. 'new.tex',
  743. 2,
  744. 4,
  745. (error, param) => {
  746. if (param == null) {
  747. param = {}
  748. }
  749. const { initialContent, updates } = param
  750. expect(error).to.be.null
  751. expect(initialContent).to.equal(this.fileContents)
  752. expect(updates).to.deep.equal([
  753. {
  754. op: [{ i: 'bar', p: 6 }],
  755. meta: {
  756. users: [this.author1.id],
  757. start_ts: this.date.getTime(),
  758. end_ts: this.date.getTime(),
  759. },
  760. v: 2,
  761. },
  762. {
  763. op: [{ i: 'baz', p: 9 }],
  764. meta: {
  765. users: [this.author1.id],
  766. start_ts: this.date.getTime(),
  767. end_ts: this.date.getTime(),
  768. },
  769. v: 3,
  770. },
  771. ])
  772. done()
  773. }
  774. )
  775. })
  776. it('returns changes when the file was created at the fromVersion', function (done) {
  777. this.ChunkTranslator.convertToDiffUpdates(
  778. this.projectId,
  779. this.chunk,
  780. 'new.tex',
  781. 1,
  782. 4,
  783. (error, param) => {
  784. if (param == null) {
  785. param = {}
  786. }
  787. const { initialContent, updates } = param
  788. expect(error).to.be.null
  789. expect(initialContent).to.equal(this.fileContents)
  790. expect(updates).to.deep.equal([
  791. {
  792. op: [{ i: 'bar', p: 6 }],
  793. meta: {
  794. users: [this.author1.id],
  795. start_ts: this.date.getTime(),
  796. end_ts: this.date.getTime(),
  797. },
  798. v: 2,
  799. },
  800. {
  801. op: [{ i: 'baz', p: 9 }],
  802. meta: {
  803. users: [this.author1.id],
  804. start_ts: this.date.getTime(),
  805. end_ts: this.date.getTime(),
  806. },
  807. v: 3,
  808. },
  809. ])
  810. done()
  811. }
  812. )
  813. })
  814. it('returns changes when the file was created after the fromVersion', function (done) {
  815. this.ChunkTranslator.convertToDiffUpdates(
  816. this.projectId,
  817. this.chunk,
  818. 'new.tex',
  819. 0,
  820. 4,
  821. (error, param) => {
  822. if (param == null) {
  823. param = {}
  824. }
  825. const { initialContent, updates } = param
  826. expect(error).to.be.null
  827. expect(initialContent).to.equal(this.fileContents)
  828. expect(updates).to.deep.equal([
  829. {
  830. op: [{ i: 'bar', p: 6 }],
  831. meta: {
  832. users: [this.author1.id],
  833. start_ts: this.date.getTime(),
  834. end_ts: this.date.getTime(),
  835. },
  836. v: 2,
  837. },
  838. {
  839. op: [{ i: 'baz', p: 9 }],
  840. meta: {
  841. users: [this.author1.id],
  842. start_ts: this.date.getTime(),
  843. end_ts: this.date.getTime(),
  844. },
  845. v: 3,
  846. },
  847. ])
  848. done()
  849. }
  850. )
  851. })
  852. })
  853. describe('convertToSummarizedUpdates', function () {
  854. it('should return a summary which includes the addition', function (done) {
  855. const assertion = (error, updates) => {
  856. expect(error).to.be.null
  857. expect(updates).to.deep.equal([
  858. {
  859. pathnames: ['main.tex'],
  860. project_ops: [],
  861. meta: {
  862. users: [this.author1.id],
  863. start_ts: this.date.getTime(),
  864. end_ts: this.date.getTime(),
  865. },
  866. v: 0,
  867. },
  868. {
  869. pathnames: [],
  870. project_ops: [
  871. {
  872. add: {
  873. pathname: 'new.tex',
  874. },
  875. },
  876. ],
  877. meta: {
  878. users: [this.author1.id],
  879. start_ts: this.date.getTime(),
  880. end_ts: this.date.getTime(),
  881. },
  882. v: 1,
  883. },
  884. {
  885. pathnames: ['new.tex'],
  886. project_ops: [],
  887. meta: {
  888. users: [this.author1.id],
  889. start_ts: this.date.getTime(),
  890. end_ts: this.date.getTime(),
  891. },
  892. v: 2,
  893. },
  894. {
  895. pathnames: ['new.tex'],
  896. project_ops: [],
  897. meta: {
  898. users: [this.author1.id],
  899. start_ts: this.date.getTime(),
  900. end_ts: this.date.getTime(),
  901. },
  902. v: 3,
  903. },
  904. ])
  905. done()
  906. }
  907. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  908. })
  909. })
  910. })
  911. describe('when the file is renamed during the chunk', function () {
  912. beforeEach(function () {
  913. this.chunk = {
  914. chunk: {
  915. startVersion: 0,
  916. history: {
  917. snapshot: {
  918. files: {
  919. 'main.tex': {
  920. hash: this.fileHash,
  921. stringLength: 42,
  922. },
  923. },
  924. },
  925. changes: [
  926. {
  927. operations: [{ pathname: 'main.tex', textOperation: ['foo'] }],
  928. timestamp: this.date.toISOString(),
  929. authors: [this.author1.id],
  930. },
  931. {
  932. operations: [
  933. { pathname: 'main.tex', newPathname: 'moved.tex' },
  934. ],
  935. timestamp: this.date.toISOString(),
  936. authors: [this.author1.id],
  937. },
  938. {
  939. operations: [
  940. { pathname: 'moved.tex', textOperation: [3, 'bar'] },
  941. ],
  942. timestamp: this.date.toISOString(),
  943. authors: [this.author1.id],
  944. },
  945. {
  946. operations: [
  947. { pathname: 'moved.tex', newPathname: 'moved_again.tex' },
  948. ],
  949. timestamp: this.date.toISOString(),
  950. authors: [this.author1.id],
  951. },
  952. {
  953. operations: [
  954. { pathname: 'moved_again.tex', textOperation: [6, 'baz'] },
  955. ],
  956. timestamp: this.date.toISOString(),
  957. authors: [this.author1.id],
  958. },
  959. ],
  960. },
  961. },
  962. authors: [this.author1],
  963. }
  964. })
  965. describe('convertToDiffUpdates', function () {
  966. it('uses the original pathname before it is moved', function (done) {
  967. this.ChunkTranslator.convertToDiffUpdates(
  968. this.projectId,
  969. this.chunk,
  970. 'main.tex',
  971. 0,
  972. 5,
  973. (error, param) => {
  974. if (param == null) {
  975. param = {}
  976. }
  977. const { initialContent, updates } = param
  978. expect(error).to.be.null
  979. expect(initialContent).to.equal(this.fileContents)
  980. expect(updates).to.deep.equal([
  981. {
  982. op: [{ i: 'foo', p: 0 }],
  983. meta: {
  984. users: [this.author1.id],
  985. start_ts: this.date.getTime(),
  986. end_ts: this.date.getTime(),
  987. },
  988. v: 0,
  989. },
  990. {
  991. op: [{ i: 'bar', p: 3 }],
  992. meta: {
  993. users: [this.author1.id],
  994. start_ts: this.date.getTime(),
  995. end_ts: this.date.getTime(),
  996. },
  997. v: 2,
  998. },
  999. {
  1000. op: [{ i: 'baz', p: 6 }],
  1001. meta: {
  1002. users: [this.author1.id],
  1003. start_ts: this.date.getTime(),
  1004. end_ts: this.date.getTime(),
  1005. },
  1006. v: 4,
  1007. },
  1008. ])
  1009. done()
  1010. }
  1011. )
  1012. })
  1013. it('uses the original pathname for before the move change', function (done) {
  1014. this.ChunkTranslator.convertToDiffUpdates(
  1015. this.projectId,
  1016. this.chunk,
  1017. 'main.tex',
  1018. 1,
  1019. 5,
  1020. (error, param) => {
  1021. if (param == null) {
  1022. param = {}
  1023. }
  1024. const { initialContent, updates } = param
  1025. expect(error).to.be.null
  1026. expect(initialContent).to.equal('foo' + this.fileContents)
  1027. expect(updates).to.deep.equal([
  1028. {
  1029. op: [{ i: 'bar', p: 3 }],
  1030. meta: {
  1031. users: [this.author1.id],
  1032. start_ts: this.date.getTime(),
  1033. end_ts: this.date.getTime(),
  1034. },
  1035. v: 2,
  1036. },
  1037. {
  1038. op: [{ i: 'baz', p: 6 }],
  1039. meta: {
  1040. users: [this.author1.id],
  1041. start_ts: this.date.getTime(),
  1042. end_ts: this.date.getTime(),
  1043. },
  1044. v: 4,
  1045. },
  1046. ])
  1047. done()
  1048. }
  1049. )
  1050. })
  1051. it('uses the new pathname for after the move change', function (done) {
  1052. this.ChunkTranslator.convertToDiffUpdates(
  1053. this.projectId,
  1054. this.chunk,
  1055. 'moved.tex',
  1056. 2,
  1057. 5,
  1058. (error, param) => {
  1059. if (param == null) {
  1060. param = {}
  1061. }
  1062. const { initialContent, updates } = param
  1063. expect(error).to.be.null
  1064. expect(initialContent).to.equal('foo' + this.fileContents)
  1065. expect(updates).to.deep.equal([
  1066. {
  1067. op: [{ i: 'bar', p: 3 }],
  1068. meta: {
  1069. users: [this.author1.id],
  1070. start_ts: this.date.getTime(),
  1071. end_ts: this.date.getTime(),
  1072. },
  1073. v: 2,
  1074. },
  1075. {
  1076. op: [{ i: 'baz', p: 6 }],
  1077. meta: {
  1078. users: [this.author1.id],
  1079. start_ts: this.date.getTime(),
  1080. end_ts: this.date.getTime(),
  1081. },
  1082. v: 4,
  1083. },
  1084. ])
  1085. done()
  1086. }
  1087. )
  1088. })
  1089. it('tracks multiple renames', function (done) {
  1090. this.ChunkTranslator.convertToDiffUpdates(
  1091. this.projectId,
  1092. this.chunk,
  1093. 'moved_again.tex',
  1094. 4,
  1095. 5,
  1096. (error, param) => {
  1097. if (param == null) {
  1098. param = {}
  1099. }
  1100. const { initialContent, updates } = param
  1101. expect(error).to.be.null
  1102. expect(initialContent).to.equal('foobar' + this.fileContents)
  1103. expect(updates).to.deep.equal([
  1104. {
  1105. op: [{ i: 'baz', p: 6 }],
  1106. meta: {
  1107. users: [this.author1.id],
  1108. start_ts: this.date.getTime(),
  1109. end_ts: this.date.getTime(),
  1110. },
  1111. v: 4,
  1112. },
  1113. ])
  1114. done()
  1115. }
  1116. )
  1117. })
  1118. it('returns an error when referring to a file that is now moved', function (done) {
  1119. this.ChunkTranslator.convertToDiffUpdates(
  1120. this.projectId,
  1121. this.chunk,
  1122. 'main.tex',
  1123. 4,
  1124. 5,
  1125. error => {
  1126. expect(error.message).to.equal(
  1127. "pathname 'main.tex' not found in range"
  1128. )
  1129. done()
  1130. }
  1131. )
  1132. })
  1133. })
  1134. describe('convertToSummarizedUpdates', function () {
  1135. it('should return a summary which includes the rename', function (done) {
  1136. const assertion = (error, updates) => {
  1137. expect(error).to.be.null
  1138. expect(updates).to.deep.equal([
  1139. {
  1140. pathnames: ['main.tex'],
  1141. project_ops: [],
  1142. meta: {
  1143. users: [this.author1.id],
  1144. start_ts: this.date.getTime(),
  1145. end_ts: this.date.getTime(),
  1146. },
  1147. v: 0,
  1148. },
  1149. {
  1150. pathnames: [],
  1151. project_ops: [
  1152. {
  1153. rename: {
  1154. pathname: 'main.tex',
  1155. newPathname: 'moved.tex',
  1156. },
  1157. },
  1158. ],
  1159. meta: {
  1160. users: [this.author1.id],
  1161. start_ts: this.date.getTime(),
  1162. end_ts: this.date.getTime(),
  1163. },
  1164. v: 1,
  1165. },
  1166. {
  1167. pathnames: ['moved.tex'],
  1168. project_ops: [],
  1169. meta: {
  1170. users: [this.author1.id],
  1171. start_ts: this.date.getTime(),
  1172. end_ts: this.date.getTime(),
  1173. },
  1174. v: 2,
  1175. },
  1176. {
  1177. pathnames: [],
  1178. project_ops: [
  1179. {
  1180. rename: {
  1181. pathname: 'moved.tex',
  1182. newPathname: 'moved_again.tex',
  1183. },
  1184. },
  1185. ],
  1186. meta: {
  1187. users: [this.author1.id],
  1188. start_ts: this.date.getTime(),
  1189. end_ts: this.date.getTime(),
  1190. },
  1191. v: 3,
  1192. },
  1193. {
  1194. pathnames: ['moved_again.tex'],
  1195. project_ops: [],
  1196. meta: {
  1197. users: [this.author1.id],
  1198. start_ts: this.date.getTime(),
  1199. end_ts: this.date.getTime(),
  1200. },
  1201. v: 4,
  1202. },
  1203. ])
  1204. done()
  1205. }
  1206. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  1207. })
  1208. })
  1209. })
  1210. describe('when the file is deleted during the chunk', function () {
  1211. beforeEach(function () {
  1212. this.chunk = {
  1213. chunk: {
  1214. startVersion: 0,
  1215. history: {
  1216. snapshot: {
  1217. files: {
  1218. 'main.tex': {
  1219. hash: this.fileHash,
  1220. stringLength: 42,
  1221. },
  1222. 'other.tex': {
  1223. hash: this.fileHash,
  1224. stringLength: 42,
  1225. },
  1226. },
  1227. },
  1228. changes: [
  1229. {
  1230. operations: [{ pathname: 'main.tex', textOperation: ['foo'] }],
  1231. timestamp: this.date.toISOString(),
  1232. authors: [this.author1.id],
  1233. },
  1234. {
  1235. operations: [{ pathname: 'main.tex', newPathname: '' }],
  1236. timestamp: this.date.toISOString(),
  1237. authors: [this.author1.id],
  1238. },
  1239. {
  1240. operations: [{ pathname: 'other.tex', textOperation: ['foo'] }],
  1241. timestamp: this.date.toISOString(),
  1242. authors: [this.author1.id],
  1243. },
  1244. ],
  1245. },
  1246. },
  1247. authors: [this.author1],
  1248. }
  1249. })
  1250. describe('convertToDiffUpdates', function () {
  1251. it('returns updates up to when it is deleted', function (done) {
  1252. this.ChunkTranslator.convertToDiffUpdates(
  1253. this.projectId,
  1254. this.chunk,
  1255. 'main.tex',
  1256. 0,
  1257. 3,
  1258. (error, param) => {
  1259. if (param == null) {
  1260. param = {}
  1261. }
  1262. const { initialContent, updates } = param
  1263. expect(error).to.be.null
  1264. expect(initialContent).to.equal(this.fileContents)
  1265. expect(updates).to.deep.equal([
  1266. {
  1267. op: [{ i: 'foo', p: 0 }],
  1268. meta: {
  1269. users: [this.author1.id],
  1270. start_ts: this.date.getTime(),
  1271. end_ts: this.date.getTime(),
  1272. },
  1273. v: 0,
  1274. },
  1275. ])
  1276. done()
  1277. }
  1278. )
  1279. })
  1280. it('returns nothing if fromVersion is when is it was deleted', function (done) {
  1281. this.ChunkTranslator.convertToDiffUpdates(
  1282. this.projectId,
  1283. this.chunk,
  1284. 'main.tex',
  1285. 1,
  1286. 3,
  1287. (error, param) => {
  1288. if (param == null) {
  1289. param = {}
  1290. }
  1291. const { initialContent, updates } = param
  1292. expect(error).to.be.null
  1293. expect(initialContent).to.equal('foo' + this.fileContents)
  1294. expect(updates).to.deep.equal([])
  1295. done()
  1296. }
  1297. )
  1298. })
  1299. it('returns an error requesting changes after deleted', function (done) {
  1300. this.ChunkTranslator.convertToDiffUpdates(
  1301. this.projectId,
  1302. this.chunk,
  1303. 'main.tex',
  1304. 2,
  1305. 3,
  1306. error => {
  1307. expect(error.message).to.equal(
  1308. "pathname 'main.tex' not found in range"
  1309. )
  1310. done()
  1311. }
  1312. )
  1313. })
  1314. })
  1315. describe('convertToSummarizedUpdates', function () {
  1316. it('should return a summary which includes the delete', function (done) {
  1317. const assertion = (error, updates) => {
  1318. expect(error).to.be.null
  1319. expect(updates).to.deep.equal([
  1320. {
  1321. pathnames: ['main.tex'],
  1322. project_ops: [],
  1323. meta: {
  1324. users: [this.author1.id],
  1325. start_ts: this.date.getTime(),
  1326. end_ts: this.date.getTime(),
  1327. },
  1328. v: 0,
  1329. },
  1330. {
  1331. pathnames: [],
  1332. project_ops: [
  1333. {
  1334. remove: {
  1335. pathname: 'main.tex',
  1336. },
  1337. },
  1338. ],
  1339. meta: {
  1340. users: [this.author1.id],
  1341. start_ts: this.date.getTime(),
  1342. end_ts: this.date.getTime(),
  1343. },
  1344. v: 1,
  1345. },
  1346. {
  1347. pathnames: ['other.tex'],
  1348. project_ops: [],
  1349. meta: {
  1350. users: [this.author1.id],
  1351. start_ts: this.date.getTime(),
  1352. end_ts: this.date.getTime(),
  1353. },
  1354. v: 2,
  1355. },
  1356. ])
  1357. done()
  1358. }
  1359. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  1360. })
  1361. })
  1362. })
  1363. describe("with text operations applied to files that don't exist", function (done) {
  1364. beforeEach(function () {
  1365. this.chunk = {
  1366. chunk: {
  1367. startVersion: 0,
  1368. history: {
  1369. snapshot: {
  1370. files: {
  1371. 'main.tex': {
  1372. hash: this.fileHash,
  1373. stringLength: 42,
  1374. },
  1375. },
  1376. },
  1377. changes: [
  1378. {
  1379. operations: [
  1380. {
  1381. pathname: 'not_here.tex',
  1382. textOperation: [3, 'Hello world'],
  1383. },
  1384. ],
  1385. timestamp: this.date.toISOString(),
  1386. authors: [this.author1.id, undefined],
  1387. },
  1388. ],
  1389. },
  1390. },
  1391. authors: [this.author1],
  1392. }
  1393. })
  1394. describe('convertToSummarizedUpdates', function () {
  1395. it('should return an empty diff instead of an error', function (done) {
  1396. this.ChunkTranslator.convertToDiffUpdates(
  1397. this.projectId,
  1398. this.chunk,
  1399. 'not_here.tex',
  1400. 0,
  1401. 1,
  1402. (error, result) => {
  1403. expect(error).to.equal(null)
  1404. expect(result.updates.length).to.equal(0)
  1405. done()
  1406. }
  1407. )
  1408. })
  1409. })
  1410. })
  1411. describe("with rename operations applied to files that don't exist", function (done) {
  1412. beforeEach(function () {
  1413. this.chunk = {
  1414. chunk: {
  1415. startVersion: 0,
  1416. history: {
  1417. snapshot: {
  1418. files: {
  1419. 'main.tex': {
  1420. hash: this.fileHash,
  1421. stringLength: 42,
  1422. },
  1423. },
  1424. },
  1425. changes: [
  1426. {
  1427. operations: [
  1428. { pathname: 'not_here.tex', newPathname: 'blah.tex' },
  1429. ],
  1430. timestamp: this.date.toISOString(),
  1431. authors: [this.author1.id, undefined],
  1432. },
  1433. ],
  1434. },
  1435. },
  1436. authors: [this.author1],
  1437. }
  1438. })
  1439. describe('convertToSummarizedUpdates', function () {
  1440. it('should return an empty diff instead of an error', function (done) {
  1441. this.ChunkTranslator.convertToDiffUpdates(
  1442. this.projectId,
  1443. this.chunk,
  1444. 'not_here.tex',
  1445. 0,
  1446. 1,
  1447. (error, result) => {
  1448. expect(error).to.equal(null)
  1449. expect(result.updates.length).to.equal(0)
  1450. done()
  1451. }
  1452. )
  1453. })
  1454. })
  1455. })
  1456. describe("with remove operations applied to files that don't exist", function (done) {
  1457. beforeEach(function () {
  1458. this.chunk = {
  1459. chunk: {
  1460. startVersion: 0,
  1461. history: {
  1462. snapshot: {
  1463. files: {
  1464. 'main.tex': {
  1465. hash: this.fileHash,
  1466. stringLength: 42,
  1467. },
  1468. },
  1469. },
  1470. changes: [
  1471. {
  1472. operations: [{ pathname: 'not_here.tex', newPathname: '' }],
  1473. timestamp: this.date.toISOString(),
  1474. authors: [this.author1.id, undefined],
  1475. },
  1476. ],
  1477. },
  1478. },
  1479. authors: [this.author1],
  1480. }
  1481. })
  1482. describe('convertToSummarizedUpdates', function () {
  1483. it('should return an empty diff instead of an error', function (done) {
  1484. this.ChunkTranslator.convertToDiffUpdates(
  1485. this.projectId,
  1486. this.chunk,
  1487. 'not_here.tex',
  1488. 0,
  1489. 1,
  1490. (error, result) => {
  1491. expect(error).to.equal(null)
  1492. expect(result.updates.length).to.equal(0)
  1493. done()
  1494. }
  1495. )
  1496. })
  1497. })
  1498. })
  1499. describe('with multiple operations in one change', function () {
  1500. beforeEach(function () {
  1501. this.chunk = {
  1502. chunk: {
  1503. startVersion: 0,
  1504. history: {
  1505. snapshot: {
  1506. files: {
  1507. 'main.tex': {
  1508. hash: this.fileHash,
  1509. stringLength: 42,
  1510. },
  1511. 'other.tex': {
  1512. hash: this.fileHash,
  1513. stringLength: 42,
  1514. },
  1515. 'old.tex': {
  1516. hash: this.fileHash,
  1517. stringLength: 42,
  1518. },
  1519. 'deleted.tex': {
  1520. hash: this.fileHash,
  1521. stringLength: 42,
  1522. },
  1523. },
  1524. },
  1525. changes: [
  1526. {
  1527. operations: [
  1528. { pathname: 'main.tex', textOperation: ['Hello test, ', -6] },
  1529. { pathname: 'main.tex', textOperation: [6, 'foo '] },
  1530. { pathname: 'other.tex', textOperation: [6, 'foo '] },
  1531. { pathname: 'old.tex', newPathname: 'new.tex' },
  1532. { pathname: 'deleted.tex', newPathname: '' },
  1533. {
  1534. pathname: 'created.tex',
  1535. file: { hash: this.fileHash, stringLength: 10 },
  1536. },
  1537. ],
  1538. timestamp: this.date.toISOString(),
  1539. authors: [this.author1.id],
  1540. },
  1541. {
  1542. operations: [{ pathname: 'main.tex', textOperation: [6, -4] }],
  1543. timestamp: this.date.toISOString(),
  1544. authors: [this.author1.id],
  1545. },
  1546. ],
  1547. },
  1548. },
  1549. authors: [this.author1],
  1550. }
  1551. })
  1552. describe('convertToDiffUpdates', function () {
  1553. it('should can return multiple ops from the same version', function (done) {
  1554. this.ChunkTranslator.convertToDiffUpdates(
  1555. this.projectId,
  1556. this.chunk,
  1557. 'main.tex',
  1558. 0,
  1559. 2,
  1560. (error, param) => {
  1561. if (param == null) {
  1562. param = {}
  1563. }
  1564. const { initialContent, updates } = param
  1565. expect(error).to.be.null
  1566. expect(initialContent).to.equal(this.fileContents)
  1567. expect(updates).to.deep.equal([
  1568. {
  1569. op: [
  1570. { i: 'Hello test, ', p: 0 },
  1571. { d: 'Hello ', p: 12 },
  1572. ],
  1573. meta: {
  1574. users: [this.author1.id],
  1575. start_ts: this.date.getTime(),
  1576. end_ts: this.date.getTime(),
  1577. },
  1578. v: 0,
  1579. },
  1580. {
  1581. op: [{ i: 'foo ', p: 6 }],
  1582. meta: {
  1583. users: [this.author1.id],
  1584. start_ts: this.date.getTime(),
  1585. end_ts: this.date.getTime(),
  1586. },
  1587. v: 0,
  1588. },
  1589. {
  1590. op: [{ d: 'foo ', p: 6 }],
  1591. meta: {
  1592. users: [this.author1.id],
  1593. start_ts: this.date.getTime(),
  1594. end_ts: this.date.getTime(),
  1595. },
  1596. v: 1,
  1597. },
  1598. ])
  1599. done()
  1600. }
  1601. )
  1602. })
  1603. it('should return the correct initial text if there are previous changes', function (done) {
  1604. this.ChunkTranslator.convertToDiffUpdates(
  1605. this.projectId,
  1606. this.chunk,
  1607. 'main.tex',
  1608. 1,
  1609. 2,
  1610. (error, param) => {
  1611. if (param == null) {
  1612. param = {}
  1613. }
  1614. const { initialContent, updates } = param
  1615. expect(error).to.be.null
  1616. expect(initialContent).to.equal(
  1617. 'Hello foo test, world, this is a test'
  1618. )
  1619. expect(updates).to.deep.equal([
  1620. {
  1621. op: [{ d: 'foo ', p: 6 }],
  1622. meta: {
  1623. users: [this.author1.id],
  1624. start_ts: this.date.getTime(),
  1625. end_ts: this.date.getTime(),
  1626. },
  1627. v: 1,
  1628. },
  1629. ])
  1630. done()
  1631. }
  1632. )
  1633. })
  1634. })
  1635. describe('convertToSummarizedUpdates', function () {
  1636. it('should return a summary of containing multiple changes', function (done) {
  1637. const assertion = (error, updates) => {
  1638. expect(error).to.be.null
  1639. expect(updates).to.deep.equal([
  1640. {
  1641. pathnames: ['main.tex', 'other.tex'],
  1642. project_ops: [
  1643. {
  1644. rename: {
  1645. pathname: 'old.tex',
  1646. newPathname: 'new.tex',
  1647. },
  1648. },
  1649. {
  1650. remove: {
  1651. pathname: 'deleted.tex',
  1652. },
  1653. },
  1654. {
  1655. add: {
  1656. pathname: 'created.tex',
  1657. },
  1658. },
  1659. ],
  1660. meta: {
  1661. users: [this.author1.id],
  1662. start_ts: this.date.getTime(),
  1663. end_ts: this.date.getTime(),
  1664. },
  1665. v: 0,
  1666. },
  1667. {
  1668. pathnames: ['main.tex'],
  1669. project_ops: [],
  1670. meta: {
  1671. users: [this.author1.id],
  1672. start_ts: this.date.getTime(),
  1673. end_ts: this.date.getTime(),
  1674. },
  1675. v: 1,
  1676. },
  1677. ])
  1678. done()
  1679. }
  1680. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  1681. })
  1682. })
  1683. })
  1684. describe('with a binary file', function () {
  1685. beforeEach(function () {
  1686. this.chunk = {
  1687. chunk: {
  1688. startVersion: 0,
  1689. history: {
  1690. snapshot: {
  1691. files: {
  1692. 'main.tex': {
  1693. hash: this.fileHash,
  1694. stringLength: 42,
  1695. },
  1696. 'binary.tex': {
  1697. hash: this.fileHash,
  1698. byteLength: 42,
  1699. },
  1700. },
  1701. },
  1702. changes: [
  1703. {
  1704. operations: [
  1705. { pathname: 'main.tex', textOperation: ['Hello test, ', -6] },
  1706. ],
  1707. timestamp: this.date.toISOString(),
  1708. authors: [this.author1.id],
  1709. },
  1710. ],
  1711. },
  1712. },
  1713. authors: [this.author1.id],
  1714. }
  1715. })
  1716. describe('convertToDiffUpdates', function () {
  1717. it('should convert them to a binary diff', function (done) {
  1718. this.ChunkTranslator.convertToDiffUpdates(
  1719. this.projectId,
  1720. this.chunk,
  1721. 'binary.tex',
  1722. 0,
  1723. 1,
  1724. (error, diff) => {
  1725. expect(error).to.be.null
  1726. expect(diff).to.deep.equal({ binary: true })
  1727. done()
  1728. }
  1729. )
  1730. })
  1731. })
  1732. })
  1733. describe('with v2 author ids', function () {
  1734. beforeEach(function () {
  1735. this.chunk = {
  1736. chunk: {
  1737. startVersion: 0,
  1738. history: {
  1739. snapshot: {
  1740. files: {
  1741. 'main.tex': {
  1742. hash: this.fileHash,
  1743. stringLength: 42,
  1744. },
  1745. },
  1746. },
  1747. changes: [
  1748. {
  1749. operations: [
  1750. { pathname: 'main.tex', textOperation: ['Hello test, ', -6] },
  1751. ],
  1752. timestamp: this.date.toISOString(),
  1753. v2Authors: [(this.v2AuthorId = '123456789')],
  1754. },
  1755. ],
  1756. },
  1757. },
  1758. }
  1759. })
  1760. describe('convertToDiffUpdates', function () {
  1761. it('should return the v2 author id in the users array', function (done) {
  1762. this.ChunkTranslator.convertToDiffUpdates(
  1763. this.projectId,
  1764. this.chunk,
  1765. 'main.tex',
  1766. 0,
  1767. 1,
  1768. (error, diff) => {
  1769. expect(error).to.be.null
  1770. expect(diff.updates).to.deep.equal([
  1771. {
  1772. op: [
  1773. { i: 'Hello test, ', p: 0 },
  1774. { d: 'Hello ', p: 12 },
  1775. ],
  1776. meta: {
  1777. users: [this.v2AuthorId],
  1778. start_ts: this.date.getTime(),
  1779. end_ts: this.date.getTime(),
  1780. },
  1781. v: 0,
  1782. },
  1783. ])
  1784. done()
  1785. }
  1786. )
  1787. })
  1788. })
  1789. describe('convertToSummarizedUpdates', function () {
  1790. it('should return the v2 author id in the users array', function (done) {
  1791. const assertion = (error, updateSet) => {
  1792. expect(error).to.be.null
  1793. expect(updateSet).to.deep.equal([
  1794. {
  1795. pathnames: ['main.tex'],
  1796. project_ops: [],
  1797. meta: {
  1798. users: [this.v2AuthorId],
  1799. start_ts: this.date.getTime(),
  1800. end_ts: this.date.getTime(),
  1801. },
  1802. v: 0,
  1803. },
  1804. ])
  1805. done()
  1806. }
  1807. this.ChunkTranslator.convertToSummarizedUpdates(this.chunk, assertion)
  1808. })
  1809. })
  1810. })
  1811. describe('with tracked changes in a file', function () {
  1812. describe('convertToDiffUpdates', function () {
  1813. beforeEach(function () {
  1814. this.rangesHash = 'some_ranges_hash'
  1815. this.fileContents = 'Hello planet world, this is a test'
  1816. this.ranges = JSON.stringify({
  1817. trackedChanges: [
  1818. {
  1819. range: { pos: 6, length: 7 },
  1820. tracking: {
  1821. type: 'delete',
  1822. userId: this.author1.id,
  1823. ts: '2024-01-01T00:00:00.000Z',
  1824. },
  1825. },
  1826. ],
  1827. })
  1828. this.HistoryStoreManager.getProjectBlob
  1829. .withArgs(this.historyId, this.rangesHash)
  1830. .yields(null, this.ranges)
  1831. this.HistoryStoreManager.getProjectBlob
  1832. .withArgs(this.historyId, this.fileHash)
  1833. .yields(null, this.fileContents)
  1834. })
  1835. it('should filter out the tracked deletes that were present in the chunk', function (done) {
  1836. this.chunk = {
  1837. chunk: {
  1838. startVersion: 0,
  1839. history: {
  1840. snapshot: {
  1841. files: {
  1842. 'main.tex': {
  1843. hash: this.fileHash,
  1844. rangesHash: this.rangesHash,
  1845. stringLength: 42,
  1846. },
  1847. },
  1848. },
  1849. changes: [
  1850. {
  1851. operations: [
  1852. {
  1853. pathname: 'main.tex',
  1854. textOperation: [
  1855. // [...] is a tracked delete
  1856. 28, // Hello [planet ]world, this is |a test
  1857. -1, // Hello [planet ]world, this is | test
  1858. 'the', // Hello [planet ]world, this is the| test
  1859. 5, // Hello [planet ]world, this is the test|
  1860. ],
  1861. },
  1862. ],
  1863. timestamp: this.date.toISOString(),
  1864. authors: [this.author1.id],
  1865. },
  1866. ],
  1867. },
  1868. },
  1869. authors: [this.author1.id],
  1870. }
  1871. this.ChunkTranslator.convertToDiffUpdates(
  1872. this.projectId,
  1873. this.chunk,
  1874. 'main.tex',
  1875. 0,
  1876. 1,
  1877. (error, diff) => {
  1878. expect(error).to.be.null
  1879. expect(diff.initialContent).to.equal('Hello world, this is a test')
  1880. expect(diff.updates).to.deep.equal([
  1881. {
  1882. op: [
  1883. { i: 'the', p: 21 },
  1884. { d: 'a', p: 24 },
  1885. ],
  1886. meta: {
  1887. users: [this.author1.id],
  1888. start_ts: this.date.getTime(),
  1889. end_ts: this.date.getTime(),
  1890. },
  1891. v: 0,
  1892. },
  1893. ])
  1894. done()
  1895. }
  1896. )
  1897. })
  1898. it('should filter out tracked deletes across multiple changes', function (done) {
  1899. this.chunk = {
  1900. chunk: {
  1901. startVersion: 0,
  1902. history: {
  1903. snapshot: {
  1904. files: {
  1905. 'main.tex': {
  1906. hash: this.fileHash,
  1907. rangesHash: this.rangesHash,
  1908. stringLength: 42,
  1909. },
  1910. },
  1911. },
  1912. changes: [
  1913. {
  1914. operations: [
  1915. {
  1916. pathname: 'main.tex',
  1917. textOperation: [
  1918. // [...] is a tracked delete
  1919. 28, // Hello [planet ]world, this is |a test
  1920. -1, // Hello [planet ]world, this is | test
  1921. 'the', // Hello [planet ]world, this is the| test
  1922. 5, // Hello [planet ]world, this is the test|
  1923. ],
  1924. },
  1925. {
  1926. pathname: 'main.tex',
  1927. textOperation: [
  1928. // [...] is a tracked delete
  1929. 22, // Hello [planet ]world, th|is is the test
  1930. -2, // Hello [planet ]world, th| is the test
  1931. 'at', // Hello [planet ]world, that| is the test
  1932. 12, // Hello [planet ]world, that is the test|
  1933. ],
  1934. },
  1935. ],
  1936. timestamp: this.date.toISOString(),
  1937. authors: [this.author1.id],
  1938. },
  1939. ],
  1940. },
  1941. },
  1942. authors: [this.author1.id],
  1943. }
  1944. this.ChunkTranslator.convertToDiffUpdates(
  1945. this.projectId,
  1946. this.chunk,
  1947. 'main.tex',
  1948. 0,
  1949. 1,
  1950. (error, diff) => {
  1951. expect(error).to.be.null
  1952. expect(diff.initialContent).to.equal('Hello world, this is a test')
  1953. expect(diff.updates).to.deep.equal([
  1954. {
  1955. op: [
  1956. { i: 'the', p: 21 },
  1957. { d: 'a', p: 24 },
  1958. ],
  1959. meta: {
  1960. users: [this.author1.id],
  1961. start_ts: this.date.getTime(),
  1962. end_ts: this.date.getTime(),
  1963. },
  1964. v: 0,
  1965. },
  1966. {
  1967. op: [
  1968. { i: 'at', p: 15 },
  1969. { d: 'is', p: 17 },
  1970. ],
  1971. meta: {
  1972. users: [this.author1.id],
  1973. start_ts: this.date.getTime(),
  1974. end_ts: this.date.getTime(),
  1975. },
  1976. v: 0,
  1977. },
  1978. ])
  1979. done()
  1980. }
  1981. )
  1982. })
  1983. it('should handle tracked delete in the operation', function (done) {
  1984. this.chunk = {
  1985. chunk: {
  1986. startVersion: 0,
  1987. history: {
  1988. snapshot: {
  1989. files: {
  1990. 'main.tex': {
  1991. hash: this.fileHash,
  1992. rangesHash: this.rangesHash,
  1993. stringLength: 42,
  1994. },
  1995. },
  1996. },
  1997. changes: [
  1998. {
  1999. operations: [
  2000. {
  2001. pathname: 'main.tex',
  2002. textOperation: [
  2003. // [...] is a tracked delete
  2004. 5, // Hello| [planet ]world, this is a test
  2005. {
  2006. r: 1,
  2007. tracking: {
  2008. type: 'delete',
  2009. userId: this.author1.id,
  2010. ts: '2024-01-01T00:00:00.000Z',
  2011. },
  2012. }, // Hello[ ]|[planet ]world, this is test
  2013. 7, // Hello[ ][planet ]|world, this is the test
  2014. {
  2015. r: 5,
  2016. tracking: {
  2017. type: 'delete',
  2018. userId: this.author1.id,
  2019. ts: '2024-01-01T00:00:00.000Z',
  2020. },
  2021. }, // Hello[ ][planet ][world]|, this is the test
  2022. 18, // Hello[ ][planet ][world], this is the test|
  2023. ],
  2024. },
  2025. ],
  2026. timestamp: this.date.toISOString(),
  2027. authors: [this.author1.id],
  2028. },
  2029. ],
  2030. },
  2031. },
  2032. authors: [this.author1.id],
  2033. }
  2034. this.ChunkTranslator.convertToDiffUpdates(
  2035. this.projectId,
  2036. this.chunk,
  2037. 'main.tex',
  2038. 0,
  2039. 1,
  2040. (error, diff) => {
  2041. expect(error).to.be.null
  2042. expect(diff.initialContent).to.equal('Hello world, this is a test')
  2043. expect(diff.updates).to.deep.equal([
  2044. {
  2045. op: [
  2046. { d: ' ', p: 5 },
  2047. { d: 'world', p: 5 },
  2048. ],
  2049. meta: {
  2050. users: [this.author1.id],
  2051. start_ts: this.date.getTime(),
  2052. end_ts: this.date.getTime(),
  2053. },
  2054. v: 0,
  2055. },
  2056. ])
  2057. done()
  2058. }
  2059. )
  2060. })
  2061. it('should filter out tracked deletes in insert operations', function (done) {
  2062. this.chunk = {
  2063. chunk: {
  2064. startVersion: 0,
  2065. history: {
  2066. snapshot: {
  2067. files: {
  2068. 'main.tex': {
  2069. hash: this.fileHash,
  2070. rangesHash: this.rangesHash,
  2071. stringLength: 42,
  2072. },
  2073. },
  2074. },
  2075. changes: [
  2076. {
  2077. operations: [
  2078. {
  2079. pathname: 'main.tex',
  2080. textOperation: [
  2081. // [...] is a tracked delete
  2082. 13, // Hello [planet ]|world, this is a test
  2083. {
  2084. i: 'pluto',
  2085. tracking: {
  2086. type: 'delete',
  2087. userId: this.author1.id,
  2088. ts: '2024-01-01T00:00:00.000Z',
  2089. },
  2090. }, // Hello [planet pluto]|world, this is a test
  2091. 21, // Hello [planet pluto]world, this is a test|
  2092. ],
  2093. },
  2094. ],
  2095. timestamp: this.date.toISOString(),
  2096. authors: [this.author1.id],
  2097. },
  2098. ],
  2099. },
  2100. },
  2101. authors: [this.author1.id],
  2102. }
  2103. this.ChunkTranslator.convertToDiffUpdates(
  2104. this.projectId,
  2105. this.chunk,
  2106. 'main.tex',
  2107. 0,
  2108. 1,
  2109. (error, diff) => {
  2110. expect(error).to.be.null
  2111. expect(diff.initialContent).to.equal('Hello world, this is a test')
  2112. expect(diff.updates).to.deep.equal([
  2113. {
  2114. op: [],
  2115. meta: {
  2116. users: [this.author1.id],
  2117. start_ts: this.date.getTime(),
  2118. end_ts: this.date.getTime(),
  2119. },
  2120. v: 0,
  2121. },
  2122. ])
  2123. done()
  2124. }
  2125. )
  2126. })
  2127. it('should filter out tracked deletes in delete operations', function (done) {
  2128. this.chunk = {
  2129. chunk: {
  2130. startVersion: 0,
  2131. history: {
  2132. snapshot: {
  2133. files: {
  2134. 'main.tex': {
  2135. hash: this.fileHash,
  2136. rangesHash: this.rangesHash,
  2137. stringLength: 42,
  2138. },
  2139. },
  2140. },
  2141. changes: [
  2142. {
  2143. operations: [
  2144. {
  2145. pathname: 'main.tex',
  2146. textOperation: [
  2147. // [...] is a tracked delete
  2148. 6, // Hello |[planet ]world, this is a test
  2149. -3, // Hello [|net ]world, this is a test
  2150. 6, // Hello [net ]wo|rld, this is a test
  2151. -3, // Hello [net ]wo|, this is a test
  2152. 16, // Hello [net ]wo, this is a test|
  2153. ],
  2154. },
  2155. ],
  2156. timestamp: this.date.toISOString(),
  2157. authors: [this.author1.id],
  2158. },
  2159. ],
  2160. },
  2161. },
  2162. authors: [this.author1.id],
  2163. }
  2164. this.ChunkTranslator.convertToDiffUpdates(
  2165. this.projectId,
  2166. this.chunk,
  2167. 'main.tex',
  2168. 0,
  2169. 1,
  2170. (error, diff) => {
  2171. expect(error).to.be.null
  2172. expect(diff.initialContent).to.equal('Hello world, this is a test')
  2173. expect(diff.updates).to.deep.equal([
  2174. {
  2175. op: [{ d: 'rld', p: 8 }],
  2176. meta: {
  2177. users: [this.author1.id],
  2178. start_ts: this.date.getTime(),
  2179. end_ts: this.date.getTime(),
  2180. },
  2181. v: 0,
  2182. },
  2183. ])
  2184. done()
  2185. }
  2186. )
  2187. })
  2188. it('should filter out tracked deletes in retain operations', function (done) {
  2189. this.chunk = {
  2190. chunk: {
  2191. startVersion: 0,
  2192. history: {
  2193. snapshot: {
  2194. files: {
  2195. 'main.tex': {
  2196. hash: this.fileHash,
  2197. rangesHash: this.rangesHash,
  2198. stringLength: 42,
  2199. },
  2200. },
  2201. },
  2202. changes: [
  2203. {
  2204. operations: [
  2205. {
  2206. pathname: 'main.tex',
  2207. textOperation: [
  2208. // [...] is a tracked delete
  2209. 4, // Hell|o [planet ]world, this is a test
  2210. {
  2211. r: 4,
  2212. tracking: { type: 'none' },
  2213. }, // Hello pl|[anet ]world, this is a test
  2214. {
  2215. r: 3,
  2216. tracking: {
  2217. type: 'insert',
  2218. userId: this.author1.id,
  2219. ts: '2024-01-01T00:00:00.000Z',
  2220. },
  2221. }, // Hello plane|[t ]world, this is a test
  2222. 23, // Hello plane[t ]world, this is a test|
  2223. ],
  2224. },
  2225. ],
  2226. timestamp: this.date.toISOString(),
  2227. authors: [this.author1.id],
  2228. },
  2229. ],
  2230. },
  2231. },
  2232. authors: [this.author1.id],
  2233. }
  2234. this.ChunkTranslator.convertToDiffUpdates(
  2235. this.projectId,
  2236. this.chunk,
  2237. 'main.tex',
  2238. 0,
  2239. 1,
  2240. (error, diff) => {
  2241. expect(error).to.be.null
  2242. expect(diff.initialContent).to.equal('Hello world, this is a test')
  2243. expect(diff.updates).to.deep.equal([
  2244. {
  2245. op: [
  2246. {
  2247. i: 'pl',
  2248. p: 6,
  2249. },
  2250. {
  2251. i: 'ane',
  2252. p: 8,
  2253. },
  2254. ],
  2255. meta: {
  2256. users: [this.author1.id],
  2257. start_ts: this.date.getTime(),
  2258. end_ts: this.date.getTime(),
  2259. },
  2260. v: 0,
  2261. },
  2262. ])
  2263. done()
  2264. }
  2265. )
  2266. })
  2267. it('should report tracked deletion (retains) as deletions', function (done) {
  2268. this.chunk = {
  2269. chunk: {
  2270. startVersion: 0,
  2271. history: {
  2272. snapshot: {
  2273. files: {
  2274. 'main.tex': {
  2275. hash: this.fileHash,
  2276. rangesHash: this.rangesHash,
  2277. stringLength: 42,
  2278. },
  2279. },
  2280. },
  2281. changes: [
  2282. {
  2283. operations: [
  2284. {
  2285. pathname: 'main.tex',
  2286. textOperation: [
  2287. // [...] is a tracked delete
  2288. {
  2289. r: 34,
  2290. tracking: {
  2291. type: 'delete',
  2292. userId: this.author1.id,
  2293. ts: '2024-01-01T00:00:00.000Z',
  2294. },
  2295. }, // [Hello planet world, this is a test]|
  2296. ],
  2297. },
  2298. ],
  2299. timestamp: this.date.toISOString(),
  2300. authors: [this.author1.id],
  2301. },
  2302. ],
  2303. },
  2304. },
  2305. authors: [this.author1.id],
  2306. }
  2307. this.ChunkTranslator.convertToDiffUpdates(
  2308. this.projectId,
  2309. this.chunk,
  2310. 'main.tex',
  2311. 0,
  2312. 1,
  2313. (error, diff) => {
  2314. expect(error).to.be.null
  2315. expect(diff.initialContent).to.equal('Hello world, this is a test')
  2316. expect(diff.updates).to.deep.equal([
  2317. {
  2318. op: [
  2319. {
  2320. d: 'Hello ',
  2321. p: 0,
  2322. },
  2323. {
  2324. d: 'world, this is a test',
  2325. p: 0,
  2326. },
  2327. ],
  2328. meta: {
  2329. users: [this.author1.id],
  2330. start_ts: this.date.getTime(),
  2331. end_ts: this.date.getTime(),
  2332. },
  2333. v: 0,
  2334. },
  2335. ])
  2336. done()
  2337. }
  2338. )
  2339. })
  2340. it('should properly create changes when deleting after moved track deletes', function (done) {
  2341. this.chunk = {
  2342. chunk: {
  2343. startVersion: 0,
  2344. history: {
  2345. snapshot: {
  2346. files: {
  2347. 'main.tex': {
  2348. hash: this.fileHash,
  2349. stringLength: 34,
  2350. },
  2351. },
  2352. },
  2353. changes: [
  2354. {
  2355. operations: [
  2356. {
  2357. pathname: 'main.tex',
  2358. textOperation: [
  2359. // [...] is a tracked delete
  2360. // // He[ll]o planet world, this is a test
  2361. 2,
  2362. {
  2363. r: 2,
  2364. tracking: {
  2365. type: 'delete',
  2366. userId: this.author1.id,
  2367. ts: this.date.toISOString(),
  2368. },
  2369. },
  2370. 30,
  2371. ],
  2372. },
  2373. ],
  2374. timestamp: this.date.toISOString(),
  2375. authors: [this.author1.id],
  2376. },
  2377. {
  2378. operations: [
  2379. {
  2380. pathname: 'main.tex',
  2381. textOperation: [
  2382. // [...] is a tracked delete
  2383. // {...} is a tracked insert
  2384. // He[ll]o {TEST }planet world, this is a test
  2385. 6,
  2386. {
  2387. i: 'TEST ',
  2388. tracking: {
  2389. type: 'insert',
  2390. userId: this.author1.id,
  2391. ts: this.date.toISOString(),
  2392. },
  2393. },
  2394. 28,
  2395. ],
  2396. },
  2397. ],
  2398. timestamp: this.date.toISOString(),
  2399. authors: [this.author1.id],
  2400. },
  2401. {
  2402. operations: [
  2403. {
  2404. pathname: 'main.tex',
  2405. textOperation: [
  2406. // [...] is a tracked delete
  2407. // {...} is a tracked insert
  2408. // He[ll]o {TEST }planet world, [this] is a test
  2409. 25,
  2410. {
  2411. r: 4,
  2412. tracking: {
  2413. type: 'delete',
  2414. userId: this.author1.id,
  2415. ts: this.date.toISOString(),
  2416. },
  2417. },
  2418. 10,
  2419. ],
  2420. },
  2421. ],
  2422. timestamp: this.date.toISOString(),
  2423. authors: [this.author1.id],
  2424. },
  2425. {
  2426. operations: [
  2427. {
  2428. pathname: 'main.tex',
  2429. textOperation: [
  2430. // [...] is a tracked delete
  2431. // {...} is a tracked insert
  2432. 2, // He|[ll]o {TEST }planet world, [this] is a test
  2433. -2, // He|o {TEST }planet world, [this] is a test
  2434. 2, // Heo |{TEST }planet world, [this] is a test
  2435. {
  2436. r: 5,
  2437. tracking: { type: 'none' },
  2438. }, // Heo TEST| planet world, [this] is a test
  2439. 14, // Heo TEST planet world, |[this] is a test
  2440. -4, // Heo TEST planet world, | is a test
  2441. 10, // Heo TEST planet world, is a test|
  2442. ],
  2443. },
  2444. ],
  2445. timestamp: this.date.toISOString(),
  2446. authors: [this.author1.id],
  2447. },
  2448. ],
  2449. },
  2450. },
  2451. authors: [this.author1.id],
  2452. }
  2453. this.ChunkTranslator.convertToDiffUpdates(
  2454. this.projectId,
  2455. this.chunk,
  2456. 'main.tex',
  2457. 0,
  2458. 4,
  2459. (error, diff) => {
  2460. expect(error).to.be.null
  2461. expect(diff.updates).to.deep.equal([
  2462. {
  2463. op: [
  2464. {
  2465. d: 'll',
  2466. p: 2,
  2467. },
  2468. ],
  2469. meta: {
  2470. users: [this.author1.id],
  2471. start_ts: this.date.getTime(),
  2472. end_ts: this.date.getTime(),
  2473. },
  2474. v: 0,
  2475. },
  2476. {
  2477. op: [
  2478. {
  2479. i: 'TEST ',
  2480. p: 4,
  2481. },
  2482. ],
  2483. meta: {
  2484. users: [this.author1.id],
  2485. start_ts: this.date.getTime(),
  2486. end_ts: this.date.getTime(),
  2487. },
  2488. v: 1,
  2489. },
  2490. {
  2491. op: [
  2492. {
  2493. d: 'this',
  2494. p: 23,
  2495. },
  2496. ],
  2497. meta: {
  2498. users: [this.author1.id],
  2499. start_ts: this.date.getTime(),
  2500. end_ts: this.date.getTime(),
  2501. },
  2502. v: 2,
  2503. },
  2504. {
  2505. op: [],
  2506. meta: {
  2507. users: [this.author1.id],
  2508. start_ts: this.date.getTime(),
  2509. end_ts: this.date.getTime(),
  2510. },
  2511. v: 3,
  2512. },
  2513. ])
  2514. done()
  2515. }
  2516. )
  2517. })
  2518. it('should properly create changes when retaining after moved track deletes', function (done) {
  2519. this.chunk = {
  2520. chunk: {
  2521. startVersion: 0,
  2522. history: {
  2523. snapshot: {
  2524. files: {
  2525. 'main.tex': {
  2526. hash: this.fileHash,
  2527. stringLength: 34,
  2528. },
  2529. },
  2530. },
  2531. changes: [
  2532. {
  2533. operations: [
  2534. {
  2535. pathname: 'main.tex',
  2536. textOperation: [
  2537. // [...] is a tracked delete
  2538. // // He[ll]o planet world, this is a test
  2539. 2,
  2540. {
  2541. r: 2,
  2542. tracking: {
  2543. type: 'delete',
  2544. userId: this.author1.id,
  2545. ts: this.date.toISOString(),
  2546. },
  2547. },
  2548. 30,
  2549. ],
  2550. },
  2551. ],
  2552. timestamp: this.date.toISOString(),
  2553. authors: [this.author1.id],
  2554. },
  2555. {
  2556. operations: [
  2557. {
  2558. pathname: 'main.tex',
  2559. textOperation: [
  2560. // [...] is a tracked delete
  2561. // He[ll]o planet world, [this] is a test
  2562. 20,
  2563. {
  2564. r: 4,
  2565. tracking: {
  2566. type: 'delete',
  2567. userId: this.author1.id,
  2568. ts: this.date.toISOString(),
  2569. },
  2570. },
  2571. 10,
  2572. ],
  2573. },
  2574. ],
  2575. timestamp: this.date.toISOString(),
  2576. authors: [this.author1.id],
  2577. },
  2578. {
  2579. operations: [
  2580. {
  2581. pathname: 'main.tex',
  2582. textOperation: [
  2583. // [...] is a tracked delete
  2584. // {...} is a tracked insert
  2585. // He[ll]o planet world, [this] {TEST }is a test
  2586. 25,
  2587. {
  2588. i: 'TEST ',
  2589. tracking: {
  2590. type: 'insert',
  2591. userId: this.author1.id,
  2592. ts: this.date.toISOString(),
  2593. },
  2594. },
  2595. 9,
  2596. ],
  2597. },
  2598. ],
  2599. timestamp: this.date.toISOString(),
  2600. authors: [this.author1.id],
  2601. },
  2602. {
  2603. operations: [
  2604. {
  2605. pathname: 'main.tex',
  2606. textOperation: [
  2607. // [...] is a tracked delete
  2608. // {...} is a tracked insert
  2609. 2, // He|[ll]o planet world, [this] {TEST }is a test
  2610. -2, // He|o planet world, [this] {TEST }is a test
  2611. {
  2612. r: 39,
  2613. tracking: { type: 'none' },
  2614. },
  2615. ], // He|o planet world, this TEST is a test
  2616. },
  2617. ],
  2618. timestamp: this.date.toISOString(),
  2619. authors: [this.author1.id],
  2620. },
  2621. ],
  2622. },
  2623. },
  2624. authors: [this.author1.id],
  2625. }
  2626. this.ChunkTranslator.convertToDiffUpdates(
  2627. this.projectId,
  2628. this.chunk,
  2629. 'main.tex',
  2630. 0,
  2631. 4,
  2632. (error, diff) => {
  2633. expect(error).to.be.null
  2634. expect(diff.updates).to.deep.equal([
  2635. {
  2636. op: [
  2637. {
  2638. d: 'll',
  2639. p: 2,
  2640. },
  2641. ],
  2642. meta: {
  2643. users: [this.author1.id],
  2644. start_ts: this.date.getTime(),
  2645. end_ts: this.date.getTime(),
  2646. },
  2647. v: 0,
  2648. },
  2649. {
  2650. op: [
  2651. {
  2652. d: 'this',
  2653. p: 18,
  2654. },
  2655. ],
  2656. meta: {
  2657. users: [this.author1.id],
  2658. start_ts: this.date.getTime(),
  2659. end_ts: this.date.getTime(),
  2660. },
  2661. v: 1,
  2662. },
  2663. {
  2664. op: [
  2665. {
  2666. i: 'TEST ',
  2667. p: 19,
  2668. },
  2669. ],
  2670. meta: {
  2671. users: [this.author1.id],
  2672. start_ts: this.date.getTime(),
  2673. end_ts: this.date.getTime(),
  2674. },
  2675. v: 2,
  2676. },
  2677. {
  2678. op: [
  2679. {
  2680. i: 'this',
  2681. p: 18,
  2682. },
  2683. ],
  2684. meta: {
  2685. users: [this.author1.id],
  2686. start_ts: this.date.getTime(),
  2687. end_ts: this.date.getTime(),
  2688. },
  2689. v: 3,
  2690. },
  2691. ])
  2692. done()
  2693. }
  2694. )
  2695. })
  2696. it('should handle deletion that starts before tracked delete', function (done) {
  2697. this.chunk = {
  2698. chunk: {
  2699. startVersion: 0,
  2700. history: {
  2701. snapshot: {
  2702. files: {
  2703. 'main.tex': {
  2704. hash: this.fileHash,
  2705. stringLength: 34,
  2706. },
  2707. },
  2708. },
  2709. changes: [
  2710. {
  2711. operations: [
  2712. {
  2713. pathname: 'main.tex',
  2714. textOperation: [
  2715. // [...] is a tracked delete
  2716. // Hello planet world, [this] is a test
  2717. 20,
  2718. {
  2719. r: 4,
  2720. tracking: {
  2721. type: 'delete',
  2722. userId: this.author1.id,
  2723. ts: this.date.toISOString(),
  2724. },
  2725. },
  2726. 10,
  2727. ],
  2728. },
  2729. ],
  2730. timestamp: this.date.toISOString(),
  2731. authors: [this.author1.id],
  2732. },
  2733. {
  2734. operations: [
  2735. {
  2736. pathname: 'main.tex',
  2737. textOperation: [
  2738. // [...] is a tracked delete
  2739. 5, // Hello| planet world, [this] is a test
  2740. -25, // Hellotest
  2741. 4,
  2742. ],
  2743. },
  2744. ],
  2745. timestamp: this.date.toISOString(),
  2746. authors: [this.author1.id],
  2747. },
  2748. ],
  2749. },
  2750. },
  2751. authors: [this.author1.id],
  2752. }
  2753. this.ChunkTranslator.convertToDiffUpdates(
  2754. this.projectId,
  2755. this.chunk,
  2756. 'main.tex',
  2757. 0,
  2758. 2,
  2759. (error, diff) => {
  2760. expect(error).to.be.null
  2761. expect(diff.updates).to.deep.equal([
  2762. {
  2763. op: [
  2764. {
  2765. d: 'this',
  2766. p: 20,
  2767. },
  2768. ],
  2769. meta: {
  2770. users: [this.author1.id],
  2771. start_ts: this.date.getTime(),
  2772. end_ts: this.date.getTime(),
  2773. },
  2774. v: 0,
  2775. },
  2776. {
  2777. op: [
  2778. {
  2779. d: ' planet world, ',
  2780. p: 5,
  2781. },
  2782. {
  2783. d: ' is a ',
  2784. p: 5,
  2785. },
  2786. ],
  2787. meta: {
  2788. users: [this.author1.id],
  2789. start_ts: this.date.getTime(),
  2790. end_ts: this.date.getTime(),
  2791. },
  2792. v: 1,
  2793. },
  2794. ])
  2795. done()
  2796. }
  2797. )
  2798. })
  2799. describe('whith multiple tracked deletes', function () {
  2800. beforeEach(function () {
  2801. this.fileContents = 'Hello planet world universe, this is a test'
  2802. this.ranges = JSON.stringify({
  2803. trackedChanges: [
  2804. {
  2805. range: { pos: 6, length: 7 },
  2806. tracking: {
  2807. type: 'delete',
  2808. userId: this.author1.id,
  2809. ts: '2024-01-01T00:00:00.000Z',
  2810. },
  2811. },
  2812. {
  2813. range: { pos: 18, length: 9 },
  2814. tracking: {
  2815. type: 'delete',
  2816. userId: this.author1.id,
  2817. ts: '2024-01-01T00:00:00.000Z',
  2818. },
  2819. },
  2820. ],
  2821. })
  2822. this.HistoryStoreManager.getProjectBlob
  2823. .withArgs(this.historyId, this.rangesHash)
  2824. .yields(null, this.ranges)
  2825. this.HistoryStoreManager.getProjectBlob
  2826. .withArgs(this.historyId, this.fileHash)
  2827. .yields(null, this.fileContents)
  2828. })
  2829. it('should handle a deletion that spans multiple tracked deletes', function (done) {
  2830. this.chunk = {
  2831. chunk: {
  2832. startVersion: 0,
  2833. history: {
  2834. snapshot: {
  2835. files: {
  2836. 'main.tex': {
  2837. hash: this.fileHash,
  2838. rangesHash: this.rangesHash,
  2839. stringLength: this.fileContents.length,
  2840. },
  2841. },
  2842. },
  2843. changes: [
  2844. {
  2845. operations: [
  2846. {
  2847. pathname: 'main.tex',
  2848. textOperation: [
  2849. // [...] is a tracked delete
  2850. 6, // Hello |[planet ]world[ universe], this is a test
  2851. -21, // Hello|, this is a test
  2852. 16,
  2853. ],
  2854. },
  2855. ],
  2856. timestamp: this.date.toISOString(),
  2857. authors: [this.author1.id],
  2858. },
  2859. ],
  2860. },
  2861. },
  2862. authors: [this.author1.id],
  2863. }
  2864. this.ChunkTranslator.convertToDiffUpdates(
  2865. this.projectId,
  2866. this.chunk,
  2867. 'main.tex',
  2868. 0,
  2869. 1,
  2870. (error, diff) => {
  2871. expect(error).to.be.null
  2872. expect(diff.updates).to.deep.equal([
  2873. {
  2874. op: [
  2875. {
  2876. d: 'world',
  2877. p: 6,
  2878. },
  2879. ],
  2880. meta: {
  2881. users: [this.author1.id],
  2882. start_ts: this.date.getTime(),
  2883. end_ts: this.date.getTime(),
  2884. },
  2885. v: 0,
  2886. },
  2887. ])
  2888. done()
  2889. }
  2890. )
  2891. })
  2892. it('should handle a tracked deletion that spans multiple tracked deletes', function (done) {
  2893. this.chunk = {
  2894. chunk: {
  2895. startVersion: 0,
  2896. history: {
  2897. snapshot: {
  2898. files: {
  2899. 'main.tex': {
  2900. hash: this.fileHash,
  2901. rangesHash: this.rangesHash,
  2902. stringLength: this.fileContents.length,
  2903. },
  2904. },
  2905. },
  2906. changes: [
  2907. {
  2908. operations: [
  2909. {
  2910. pathname: 'main.tex',
  2911. textOperation: [
  2912. // [...] is a tracked delete
  2913. 6, // Hello |[planet ]world[ universe], this is a test
  2914. {
  2915. r: 21,
  2916. tracking: {
  2917. type: 'delete',
  2918. userId: this.author1.id,
  2919. ts: '2024-01-01T00:00:00.000Z',
  2920. },
  2921. }, // Hello [planet world universe]|, this is a test
  2922. 16,
  2923. ],
  2924. },
  2925. ],
  2926. timestamp: this.date.toISOString(),
  2927. authors: [this.author1.id],
  2928. },
  2929. ],
  2930. },
  2931. },
  2932. authors: [this.author1.id],
  2933. }
  2934. this.ChunkTranslator.convertToDiffUpdates(
  2935. this.projectId,
  2936. this.chunk,
  2937. 'main.tex',
  2938. 0,
  2939. 1,
  2940. (error, diff) => {
  2941. expect(error).to.be.null
  2942. expect(diff.updates).to.deep.equal([
  2943. {
  2944. op: [
  2945. {
  2946. d: 'world',
  2947. p: 6,
  2948. },
  2949. ],
  2950. meta: {
  2951. users: [this.author1.id],
  2952. start_ts: this.date.getTime(),
  2953. end_ts: this.date.getTime(),
  2954. },
  2955. v: 0,
  2956. },
  2957. ])
  2958. done()
  2959. }
  2960. )
  2961. })
  2962. })
  2963. it('should handle tracked delete insert that merges with existing tracked delete before retain with none tracking', function (done) {
  2964. // Bug reproduction: when an insert with tracking type 'delete' is
  2965. // placed at the start of an existing tracked delete (same userId),
  2966. // the two tracked deletes merge. A subsequent retain with 'none'
  2967. // tracking then encounters the merged range. The sourceOffset
  2968. // (sourceCursor - result.length) is negative due to the insert
  2969. // advancing result.length without advancing sourceCursor. When
  2970. // moveBy(sourceOffset) is called on the merged range, the position
  2971. // becomes negative, triggering "OError: Invalid range".
  2972. this.fileContents = 'Hello world'
  2973. this.ranges = JSON.stringify({
  2974. trackedChanges: [
  2975. {
  2976. range: { pos: 0, length: 11 },
  2977. tracking: {
  2978. type: 'delete',
  2979. userId: this.author1.id,
  2980. ts: '2024-01-01T00:00:00.000Z',
  2981. },
  2982. },
  2983. ],
  2984. })
  2985. this.HistoryStoreManager.getProjectBlob
  2986. .withArgs(this.historyId, this.rangesHash)
  2987. .yields(null, this.ranges)
  2988. this.HistoryStoreManager.getProjectBlob
  2989. .withArgs(this.historyId, this.fileHash)
  2990. .yields(null, this.fileContents)
  2991. this.chunk = {
  2992. chunk: {
  2993. startVersion: 0,
  2994. history: {
  2995. snapshot: {
  2996. files: {
  2997. 'main.tex': {
  2998. hash: this.fileHash,
  2999. rangesHash: this.rangesHash,
  3000. stringLength: this.fileContents.length,
  3001. },
  3002. },
  3003. },
  3004. changes: [
  3005. {
  3006. operations: [
  3007. {
  3008. pathname: 'main.tex',
  3009. textOperation: [
  3010. // Initial state: [Hello world] (all tracked-deleted)
  3011. // Insert "X" tracked as delete at position 0,
  3012. // then retain all 11 chars clearing tracking
  3013. {
  3014. i: 'X',
  3015. tracking: {
  3016. type: 'delete',
  3017. userId: this.author1.id,
  3018. ts: '2024-01-01T00:00:00.000Z',
  3019. },
  3020. },
  3021. {
  3022. r: 11,
  3023. tracking: { type: 'none' },
  3024. },
  3025. ],
  3026. },
  3027. ],
  3028. timestamp: this.date.toISOString(),
  3029. authors: [this.author1.id],
  3030. },
  3031. ],
  3032. },
  3033. },
  3034. authors: [this.author1.id],
  3035. }
  3036. this.ChunkTranslator.convertToDiffUpdates(
  3037. this.projectId,
  3038. this.chunk,
  3039. 'main.tex',
  3040. 0,
  3041. 1,
  3042. (error, diff) => {
  3043. expect(error).to.be.null
  3044. expect(diff.updates.length).to.equal(1)
  3045. expect(diff.updates[0].op).to.deep.equal([
  3046. { i: 'Hello world', p: 0 },
  3047. ])
  3048. done()
  3049. }
  3050. )
  3051. })
  3052. })
  3053. })
  3054. })