queueConflictResolution.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. // @ts-nocheck
  2. import { describe, expect, test } from 'vitest'
  3. import {
  4. operationMatchesRow,
  5. resolveDeleteRowConflicts,
  6. resolveEditCellConflicts,
  7. upsertOperation,
  8. } from './queueConflictResolution'
  9. import {
  10. QueuedOperation,
  11. QueuedOperationType,
  12. type NewAddRowOperation,
  13. type NewDeleteRowOperation,
  14. type NewEditCellContentOperation,
  15. } from '@/state/table-editor-operation-queue.types'
  16. describe('operationMatchesRow', () => {
  17. const mockTable = {} as any
  18. test('should match EDIT_CELL_CONTENT operation with same row identifiers', () => {
  19. const operation: QueuedOperation = {
  20. id: 'edit_cell_content:1:name:id:1',
  21. type: QueuedOperationType.EDIT_CELL_CONTENT,
  22. tableId: 1,
  23. timestamp: Date.now(),
  24. payload: {
  25. rowIdentifiers: { id: 1 },
  26. columnName: 'name',
  27. oldValue: 'old',
  28. newValue: 'new',
  29. table: mockTable,
  30. },
  31. }
  32. expect(operationMatchesRow(operation, 1, { id: 1 })).toBe(true)
  33. })
  34. test('should not match EDIT_CELL_CONTENT operation with different row identifiers', () => {
  35. const operation: QueuedOperation = {
  36. id: 'edit_cell_content:1:name:id:1',
  37. type: QueuedOperationType.EDIT_CELL_CONTENT,
  38. tableId: 1,
  39. timestamp: Date.now(),
  40. payload: {
  41. rowIdentifiers: { id: 1 },
  42. columnName: 'name',
  43. oldValue: 'old',
  44. newValue: 'new',
  45. table: mockTable,
  46. },
  47. }
  48. expect(operationMatchesRow(operation, 1, { id: 2 })).toBe(false)
  49. })
  50. test('should not match operation from different table', () => {
  51. const operation: QueuedOperation = {
  52. id: 'edit_cell_content:1:name:id:1',
  53. type: QueuedOperationType.EDIT_CELL_CONTENT,
  54. tableId: 1,
  55. timestamp: Date.now(),
  56. payload: {
  57. rowIdentifiers: { id: 1 },
  58. columnName: 'name',
  59. oldValue: 'old',
  60. newValue: 'new',
  61. table: mockTable,
  62. },
  63. }
  64. expect(operationMatchesRow(operation, 2, { id: 1 })).toBe(false)
  65. })
  66. test('should match DELETE_ROW operation with same row identifiers', () => {
  67. const operation: QueuedOperation = {
  68. id: 'delete_row:1:id:1',
  69. type: QueuedOperationType.DELETE_ROW,
  70. tableId: 1,
  71. timestamp: Date.now(),
  72. payload: {
  73. rowIdentifiers: { id: 1 },
  74. originalRow: { idx: 1, id: 1, name: 'test' },
  75. table: mockTable,
  76. },
  77. }
  78. expect(operationMatchesRow(operation, 1, { id: 1 })).toBe(true)
  79. })
  80. test('should return false for ADD_ROW operations', () => {
  81. const operation: QueuedOperation = {
  82. id: 'add_row:1:temp123',
  83. type: QueuedOperationType.ADD_ROW,
  84. tableId: 1,
  85. timestamp: Date.now(),
  86. payload: {
  87. tempId: 'temp123',
  88. rowData: { idx: 1, __tempId: '1', name: 'new' },
  89. table: mockTable,
  90. },
  91. }
  92. expect(operationMatchesRow(operation, 1, { id: 1 })).toBe(false)
  93. })
  94. })
  95. describe('resolveDeleteRowConflicts', () => {
  96. const mockTable = {} as any
  97. test('should skip delete and remove ADD_ROW when deleting a newly added row', () => {
  98. const addRowOp: QueuedOperation = {
  99. id: 'add_row:1:-12345',
  100. type: QueuedOperationType.ADD_ROW,
  101. tableId: 1,
  102. timestamp: Date.now(),
  103. payload: {
  104. tempId: '-12345',
  105. rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
  106. table: mockTable,
  107. },
  108. }
  109. const operations = [addRowOp]
  110. const deleteOperation: NewDeleteRowOperation = {
  111. type: QueuedOperationType.DELETE_ROW,
  112. tableId: 1,
  113. payload: {
  114. rowIdentifiers: { __tempId: '-12345' },
  115. originalRow: { idx: -12345, __tempId: '-12345', name: 'new row' },
  116. table: mockTable,
  117. },
  118. }
  119. const result = resolveDeleteRowConflicts(operations, deleteOperation)
  120. expect(result.action).toBe('skip')
  121. expect(result.filteredOperations).toEqual([])
  122. })
  123. test('should skip delete and remove ADD_ROW and related EDIT_CELLs when deleting a newly added row', () => {
  124. const addRowOp: QueuedOperation = {
  125. id: 'add_row:1:-12345',
  126. type: QueuedOperationType.ADD_ROW,
  127. tableId: 1,
  128. timestamp: Date.now(),
  129. payload: {
  130. tempId: '-12345',
  131. rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
  132. table: mockTable,
  133. },
  134. }
  135. const editCellOp: QueuedOperation = {
  136. id: 'edit_cell_content:1:name:__tempId:-12345',
  137. type: QueuedOperationType.EDIT_CELL_CONTENT,
  138. tableId: 1,
  139. timestamp: Date.now(),
  140. payload: {
  141. rowIdentifiers: { __tempId: '-12345' },
  142. columnName: 'name',
  143. oldValue: 'new row',
  144. newValue: 'edited',
  145. table: mockTable,
  146. },
  147. }
  148. const otherEditOp: QueuedOperation = {
  149. id: 'edit_cell_content:1:name:id:99',
  150. type: QueuedOperationType.EDIT_CELL_CONTENT,
  151. tableId: 1,
  152. timestamp: Date.now(),
  153. payload: {
  154. rowIdentifiers: { id: 99 },
  155. columnName: 'name',
  156. oldValue: 'original',
  157. newValue: 'changed',
  158. table: mockTable,
  159. },
  160. }
  161. const operations = [addRowOp, editCellOp, otherEditOp]
  162. const deleteOperation: NewDeleteRowOperation = {
  163. type: QueuedOperationType.DELETE_ROW,
  164. tableId: 1,
  165. payload: {
  166. rowIdentifiers: { __tempId: '-12345' },
  167. originalRow: { idx: -12345, __tempId: '-12345', name: 'new row' },
  168. table: mockTable,
  169. },
  170. }
  171. const result = resolveDeleteRowConflicts(operations, deleteOperation)
  172. expect(result.action).toBe('skip')
  173. expect(result.filteredOperations).toHaveLength(1)
  174. expect(result.filteredOperations[0]).toEqual(otherEditOp)
  175. })
  176. test('should add delete and remove EDIT_CELLs for existing row being deleted', () => {
  177. const editCellOp: QueuedOperation = {
  178. id: 'edit_cell_content:1:name:id:1',
  179. type: QueuedOperationType.EDIT_CELL_CONTENT,
  180. tableId: 1,
  181. timestamp: Date.now(),
  182. payload: {
  183. rowIdentifiers: { id: 1 },
  184. columnName: 'name',
  185. oldValue: 'original',
  186. newValue: 'edited',
  187. table: mockTable,
  188. },
  189. }
  190. const otherEditOp: QueuedOperation = {
  191. id: 'edit_cell_content:1:name:id:2',
  192. type: QueuedOperationType.EDIT_CELL_CONTENT,
  193. tableId: 1,
  194. timestamp: Date.now(),
  195. payload: {
  196. rowIdentifiers: { id: 2 },
  197. columnName: 'name',
  198. oldValue: 'other',
  199. newValue: 'changed',
  200. table: mockTable,
  201. },
  202. }
  203. const operations = [editCellOp, otherEditOp]
  204. const deleteOperation: NewDeleteRowOperation = {
  205. type: QueuedOperationType.DELETE_ROW,
  206. tableId: 1,
  207. payload: {
  208. rowIdentifiers: { id: 1 },
  209. originalRow: { idx: 1, id: 1, name: 'original' },
  210. table: mockTable,
  211. },
  212. }
  213. const result = resolveDeleteRowConflicts(operations, deleteOperation)
  214. expect(result.action).toBe('add')
  215. expect(result.filteredOperations).toHaveLength(1)
  216. expect(result.filteredOperations[0]).toEqual(otherEditOp)
  217. })
  218. test('should add delete with no changes when there are no conflicts', () => {
  219. const otherEditOp: QueuedOperation = {
  220. id: 'edit_cell_content:1:name:id:2',
  221. type: QueuedOperationType.EDIT_CELL_CONTENT,
  222. tableId: 1,
  223. timestamp: Date.now(),
  224. payload: {
  225. rowIdentifiers: { id: 2 },
  226. columnName: 'name',
  227. oldValue: 'other',
  228. newValue: 'changed',
  229. table: mockTable,
  230. },
  231. }
  232. const operations = [otherEditOp]
  233. const deleteOperation: NewDeleteRowOperation = {
  234. type: QueuedOperationType.DELETE_ROW,
  235. tableId: 1,
  236. payload: {
  237. rowIdentifiers: { id: 1 },
  238. originalRow: { idx: 1, id: 1, name: 'original' },
  239. table: mockTable,
  240. },
  241. }
  242. const result = resolveDeleteRowConflicts(operations, deleteOperation)
  243. expect(result.action).toBe('add')
  244. expect(result.filteredOperations).toEqual(operations)
  245. })
  246. })
  247. describe('resolveEditCellConflicts', () => {
  248. const mockTable = {} as any
  249. test('should reject edit on a row pending deletion', () => {
  250. const deleteOp: QueuedOperation = {
  251. id: 'delete_row:1:id:1',
  252. type: QueuedOperationType.DELETE_ROW,
  253. tableId: 1,
  254. timestamp: Date.now(),
  255. payload: {
  256. rowIdentifiers: { id: 1 },
  257. originalRow: { idx: 1, id: 1, name: 'to delete' },
  258. table: mockTable,
  259. },
  260. }
  261. const operations = [deleteOp]
  262. const editOperation: NewEditCellContentOperation = {
  263. type: QueuedOperationType.EDIT_CELL_CONTENT,
  264. tableId: 1,
  265. payload: {
  266. rowIdentifiers: { id: 1 },
  267. columnName: 'name',
  268. oldValue: 'to delete',
  269. newValue: 'changed',
  270. table: mockTable,
  271. },
  272. }
  273. const result = resolveEditCellConflicts(operations, editOperation)
  274. expect(result.action).toBe('reject')
  275. if (result.action === 'reject') {
  276. expect(result.reason).toContain('pending deletion')
  277. }
  278. })
  279. test('should merge edit into ADD_ROW for a newly added row', () => {
  280. const addRowOp: QueuedOperation = {
  281. id: 'add_row:1:-12345',
  282. type: QueuedOperationType.ADD_ROW,
  283. tableId: 1,
  284. timestamp: Date.now(),
  285. payload: {
  286. tempId: '-12345',
  287. rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
  288. table: mockTable,
  289. },
  290. }
  291. const operations = [addRowOp]
  292. const editOperation: NewEditCellContentOperation = {
  293. type: QueuedOperationType.EDIT_CELL_CONTENT,
  294. tableId: 1,
  295. payload: {
  296. rowIdentifiers: { __tempId: '-12345' },
  297. columnName: 'name',
  298. oldValue: 'new row',
  299. newValue: 'edited value',
  300. table: mockTable,
  301. },
  302. }
  303. const result = resolveEditCellConflicts(operations, editOperation)
  304. expect(result.action).toBe('merge')
  305. if (result.action === 'merge') {
  306. expect(result.updatedOperations).toHaveLength(1)
  307. const updatedAddRow = result.updatedOperations[0]
  308. expect(updatedAddRow.type).toBe(QueuedOperationType.ADD_ROW)
  309. expect((updatedAddRow.payload as any).rowData.name).toBe('edited value')
  310. }
  311. })
  312. test('should return add action for normal edit on existing row', () => {
  313. const otherOp: QueuedOperation = {
  314. id: 'edit_cell_content:1:email:id:2',
  315. type: QueuedOperationType.EDIT_CELL_CONTENT,
  316. tableId: 1,
  317. timestamp: Date.now(),
  318. payload: {
  319. rowIdentifiers: { id: 2 },
  320. columnName: 'email',
  321. oldValue: 'old@test.com',
  322. newValue: 'new@test.com',
  323. table: mockTable,
  324. },
  325. }
  326. const operations = [otherOp]
  327. const editOperation: NewEditCellContentOperation = {
  328. type: QueuedOperationType.EDIT_CELL_CONTENT,
  329. tableId: 1,
  330. payload: {
  331. rowIdentifiers: { id: 1 },
  332. columnName: 'name',
  333. oldValue: 'original',
  334. newValue: 'changed',
  335. table: mockTable,
  336. },
  337. }
  338. const result = resolveEditCellConflicts(operations, editOperation)
  339. expect(result.action).toBe('add')
  340. })
  341. test('should return add when editing tempId row but ADD_ROW not found', () => {
  342. const operations: QueuedOperation[] = []
  343. const editOperation: NewEditCellContentOperation = {
  344. type: QueuedOperationType.EDIT_CELL_CONTENT,
  345. tableId: 1,
  346. payload: {
  347. rowIdentifiers: { __tempId: '-99999' },
  348. columnName: 'name',
  349. oldValue: 'original',
  350. newValue: 'changed',
  351. table: mockTable,
  352. },
  353. }
  354. const result = resolveEditCellConflicts(operations, editOperation)
  355. expect(result.action).toBe('add')
  356. })
  357. })
  358. describe('upsertOperation', () => {
  359. const mockTable = {} as any
  360. test('should add new operation to empty queue', () => {
  361. const operations: QueuedOperation[] = []
  362. const newOperation: NewEditCellContentOperation = {
  363. type: QueuedOperationType.EDIT_CELL_CONTENT,
  364. tableId: 1,
  365. payload: {
  366. rowIdentifiers: { id: 1 },
  367. columnName: 'name',
  368. oldValue: 'original',
  369. newValue: 'changed',
  370. table: mockTable,
  371. },
  372. }
  373. const result = upsertOperation(operations, newOperation)
  374. expect(result.operations).toHaveLength(1)
  375. expect(result.operations[0].type).toBe(QueuedOperationType.EDIT_CELL_CONTENT)
  376. expect(result.operations[0].id).toBe('edit_cell_content:1:name:id:1')
  377. })
  378. test('should add new operation to existing queue', () => {
  379. const existingOp: QueuedOperation = {
  380. id: 'edit_cell_content:1:email:id:1',
  381. type: QueuedOperationType.EDIT_CELL_CONTENT,
  382. tableId: 1,
  383. timestamp: Date.now(),
  384. payload: {
  385. rowIdentifiers: { id: 1 },
  386. columnName: 'email',
  387. oldValue: 'old@test.com',
  388. newValue: 'new@test.com',
  389. table: mockTable,
  390. },
  391. }
  392. const operations = [existingOp]
  393. const newOperation: NewEditCellContentOperation = {
  394. type: QueuedOperationType.EDIT_CELL_CONTENT,
  395. tableId: 1,
  396. payload: {
  397. rowIdentifiers: { id: 1 },
  398. columnName: 'name',
  399. oldValue: 'original',
  400. newValue: 'changed',
  401. table: mockTable,
  402. },
  403. }
  404. const result = upsertOperation(operations, newOperation)
  405. expect(result.operations).toHaveLength(2)
  406. })
  407. test('should update existing EDIT_CELL operation and preserve original oldValue', () => {
  408. const existingOp: QueuedOperation = {
  409. id: 'edit_cell_content:1:name:id:1',
  410. type: QueuedOperationType.EDIT_CELL_CONTENT,
  411. tableId: 1,
  412. timestamp: Date.now() - 1000,
  413. payload: {
  414. rowIdentifiers: { id: 1 },
  415. columnName: 'name',
  416. oldValue: 'very first value',
  417. newValue: 'intermediate',
  418. table: mockTable,
  419. },
  420. }
  421. const operations = [existingOp]
  422. const newOperation: NewEditCellContentOperation = {
  423. type: QueuedOperationType.EDIT_CELL_CONTENT,
  424. tableId: 1,
  425. payload: {
  426. rowIdentifiers: { id: 1 },
  427. columnName: 'name',
  428. oldValue: 'intermediate',
  429. newValue: 'final value',
  430. table: mockTable,
  431. },
  432. }
  433. const result = upsertOperation(operations, newOperation)
  434. expect(result.operations).toHaveLength(1)
  435. const updated = result.operations[0]
  436. expect((updated.payload as any).oldValue).toBe('very first value')
  437. expect((updated.payload as any).newValue).toBe('final value')
  438. })
  439. test('should remove operation when value is reverted to original', () => {
  440. const existingOp: QueuedOperation = {
  441. id: 'edit_cell_content:1:name:id:1',
  442. type: QueuedOperationType.EDIT_CELL_CONTENT,
  443. tableId: 1,
  444. timestamp: Date.now() - 1000,
  445. payload: {
  446. rowIdentifiers: { id: 1 },
  447. columnName: 'name',
  448. oldValue: 'original',
  449. newValue: 'changed',
  450. table: mockTable,
  451. },
  452. }
  453. const otherOp: QueuedOperation = {
  454. id: 'edit_cell_content:1:email:id:1',
  455. type: QueuedOperationType.EDIT_CELL_CONTENT,
  456. tableId: 1,
  457. timestamp: Date.now(),
  458. payload: {
  459. rowIdentifiers: { id: 1 },
  460. columnName: 'email',
  461. oldValue: 'old@test.com',
  462. newValue: 'new@test.com',
  463. table: mockTable,
  464. },
  465. }
  466. const operations = [existingOp, otherOp]
  467. const newOperation: NewEditCellContentOperation = {
  468. type: QueuedOperationType.EDIT_CELL_CONTENT,
  469. tableId: 1,
  470. payload: {
  471. rowIdentifiers: { id: 1 },
  472. columnName: 'name',
  473. oldValue: 'changed',
  474. newValue: 'original',
  475. table: mockTable,
  476. },
  477. }
  478. const result = upsertOperation(operations, newOperation)
  479. expect(result.operations).toHaveLength(1)
  480. expect(result.operations[0]).toEqual(otherOp)
  481. })
  482. test('should remove operation when number oldValue matches string newValue', () => {
  483. const existingOp: QueuedOperation = {
  484. id: 'edit_cell_content:1:age:id:1',
  485. type: QueuedOperationType.EDIT_CELL_CONTENT,
  486. tableId: 1,
  487. timestamp: Date.now() - 1000,
  488. payload: {
  489. rowIdentifiers: { id: 1 },
  490. columnName: 'age',
  491. oldValue: 42,
  492. newValue: 'changed',
  493. table: mockTable,
  494. },
  495. }
  496. const operations = [existingOp]
  497. const newOperation: NewEditCellContentOperation = {
  498. type: QueuedOperationType.EDIT_CELL_CONTENT,
  499. tableId: 1,
  500. payload: {
  501. rowIdentifiers: { id: 1 },
  502. columnName: 'age',
  503. oldValue: 'changed',
  504. newValue: '42',
  505. table: mockTable,
  506. },
  507. }
  508. const result = upsertOperation(operations, newOperation)
  509. expect(result.operations).toHaveLength(0)
  510. })
  511. test('should remove operation when string oldValue matches JSON object newValue', () => {
  512. const existingOp: QueuedOperation = {
  513. id: 'edit_cell_content:1:metadata:id:1',
  514. type: QueuedOperationType.EDIT_CELL_CONTENT,
  515. tableId: 1,
  516. timestamp: Date.now() - 1000,
  517. payload: {
  518. rowIdentifiers: { id: 1 },
  519. columnName: 'metadata',
  520. oldValue: '{"key":"value","count":3}',
  521. newValue: 'changed',
  522. table: mockTable,
  523. },
  524. }
  525. const operations = [existingOp]
  526. const newOperation: NewEditCellContentOperation = {
  527. type: QueuedOperationType.EDIT_CELL_CONTENT,
  528. tableId: 1,
  529. payload: {
  530. rowIdentifiers: { id: 1 },
  531. columnName: 'metadata',
  532. oldValue: 'changed',
  533. newValue: { key: 'value', count: 3 },
  534. table: mockTable,
  535. },
  536. }
  537. const result = upsertOperation(operations, newOperation)
  538. expect(result.operations).toHaveLength(0)
  539. })
  540. test('should update existing DELETE_ROW operation', () => {
  541. const existingOp: QueuedOperation = {
  542. id: 'delete_row:1:id:1',
  543. type: QueuedOperationType.DELETE_ROW,
  544. tableId: 1,
  545. timestamp: Date.now() - 1000,
  546. payload: {
  547. rowIdentifiers: { id: 1 },
  548. originalRow: { idx: 1, id: 1, name: 'old data' },
  549. table: mockTable,
  550. },
  551. }
  552. const operations = [existingOp]
  553. const newOperation: NewDeleteRowOperation = {
  554. type: QueuedOperationType.DELETE_ROW,
  555. tableId: 1,
  556. payload: {
  557. rowIdentifiers: { id: 1 },
  558. originalRow: { idx: 1, id: 1, name: 'updated data' },
  559. table: mockTable,
  560. },
  561. }
  562. const result = upsertOperation(operations, newOperation)
  563. expect(result.operations).toHaveLength(1)
  564. expect((result.operations[0].payload as any).originalRow.name).toBe('updated data')
  565. })
  566. test('should not mutate original operations array', () => {
  567. const existingOp: QueuedOperation = {
  568. id: 'edit_cell_content:1:name:id:1',
  569. type: QueuedOperationType.EDIT_CELL_CONTENT,
  570. tableId: 1,
  571. timestamp: Date.now(),
  572. payload: {
  573. rowIdentifiers: { id: 1 },
  574. columnName: 'name',
  575. oldValue: 'original',
  576. newValue: 'changed',
  577. table: mockTable,
  578. },
  579. }
  580. const operations = [existingOp]
  581. const originalOperations = [...operations]
  582. const newOperation: NewEditCellContentOperation = {
  583. type: QueuedOperationType.EDIT_CELL_CONTENT,
  584. tableId: 1,
  585. payload: {
  586. rowIdentifiers: { id: 2 },
  587. columnName: 'name',
  588. oldValue: 'original2',
  589. newValue: 'changed2',
  590. table: mockTable,
  591. },
  592. }
  593. upsertOperation(operations, newOperation)
  594. expect(operations).toEqual(originalOperations)
  595. })
  596. test('should handle ADD_ROW operation', () => {
  597. const operations: QueuedOperation[] = []
  598. const newOperation: NewAddRowOperation = {
  599. type: QueuedOperationType.ADD_ROW,
  600. tableId: 1,
  601. payload: {
  602. tempId: '-12345',
  603. rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
  604. table: mockTable,
  605. },
  606. }
  607. const result = upsertOperation(operations, newOperation)
  608. expect(result.operations).toHaveLength(1)
  609. expect(result.operations[0].id).toBe('add_row:1:-12345')
  610. })
  611. })