|
28 | 28 | * (f) an empty authored label is stored as the humanized machine name, never |
29 | 29 | * `''`, at open; |
30 | 30 | * (g) every row a boot fills keeps its own `updated_at` exactly — the request |
31 | | - * lists sort on it, and a derived display column is not an edit. |
| 31 | + * lists sort on it, and a derived display column is not an edit; |
| 32 | + * (h) a live write landing between a page's read and its row writes — a |
| 33 | + * recall, an SLA-style slate change — is kept exactly as it left the row, |
| 34 | + * and the rest of the page is still filled with its stamps kept. |
32 | 35 | */ |
33 | 36 |
|
34 | 37 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; |
@@ -199,21 +202,27 @@ describe('#23889 — the boot-time display-label backfill', () => { |
199 | 202 |
|
200 | 203 | it('(c) never overwrites a column a live writer filled; an orphaned action keeps a null pointer and its own step name', async () => { |
201 | 204 | await engine.update('sys_approval_request', { id: 'areq_open', process_label: 'Live label' }, { context: SYSTEM } as any); |
| 205 | + // A second pending request on the same page, so one labeller call per page |
| 206 | + // and one per row give different calls. |
| 207 | + await engine.insert('sys_approval_request', legacyRequest('open2', { |
| 208 | + status: 'pending', pending_approvers: 'u_mail', node_config_json: JSON.stringify({ approvers: [] }), |
| 209 | + }), { context: SYSTEM } as any); |
202 | 210 | // An action whose step label and acting-as label a writer already stored, |
203 | 211 | // different from what its request would give: the fill copies only its |
204 | 212 | // null pointer columns and leaves both labels as stored. |
205 | 213 | await engine.update('sys_approval_action', { |
206 | 214 | id: 'a_vote', step_label: 'Recorded step', acted_as_label: 'Recorded name', |
207 | 215 | }, { context: SYSTEM } as any); |
208 | 216 | const svc = new ApprovalService({ engine: engine as any, logger: quiet as any }); |
209 | | - // The labeller is asked once per page, never once per row: here one request |
210 | | - // page (the pending slate) and one action page (two acted_as slots). |
| 217 | + // The labeller is asked once per page, never once per row: one request page |
| 218 | + // (two pending slates) and one action page. |
211 | 219 | const source = slotLabelSourceOf(svc); |
212 | 220 | const asked: string[][] = []; |
213 | 221 | const counting = { slotLabels: (slots: readonly string[]) => { asked.push([...slots]); return source.slotLabels(slots); } }; |
214 | 222 | const out = await backfillApprovalDisplayLabels(engine as any, counting); |
215 | | - expect(asked).toEqual([['u_alice', 'position:legal'], ['position:legal']]); |
216 | | - expect(out.requestsFilled).toBe(2); |
| 223 | + expect(asked).toEqual([['u_alice', 'position:legal', 'u_mail'], ['position:legal']]); |
| 224 | + expect(out.requestsFilled).toBe(3); |
| 225 | + expect((await row('sys_approval_request', 'areq_open2')).pending_approver_labels).toBe('Mia Mail'); |
217 | 226 |
|
218 | 227 | expect((await row('sys_approval_request', 'areq_open')).process_label).toBe('Live label'); |
219 | 228 | expect(await row('sys_approval_action', 'a_vote')).toMatchObject({ |
@@ -320,4 +329,57 @@ describe('#23889 — the boot-time display-label backfill', () => { |
320 | 329 | expect(await row('sys_approval_action', id), id).toMatchObject({ object_name: 'crm_deal', updated_at: BACKDATED }); |
321 | 330 | } |
322 | 331 | }); |
| 332 | + |
| 333 | + it('(h) a live write between the page read and the row writes is kept as it left the row', async () => { |
| 334 | + // Two more pending requests on the page: one a sweep recalls, one it moves to another slate. |
| 335 | + await engine.insert('sys_approval_request', legacyRequest('recalled', { |
| 336 | + status: 'pending', pending_approvers: 'u_alice', node_config_json: JSON.stringify({ approvers: [] }), |
| 337 | + }), { context: SYSTEM } as any); |
| 338 | + await engine.insert('sys_approval_request', legacyRequest('moved', { |
| 339 | + status: 'pending', pending_approvers: 'u_alice', node_config_json: JSON.stringify({ approvers: [] }), |
| 340 | + }), { context: SYSTEM } as any); |
| 341 | + for (const id of ['areq_done', 'areq_open', 'areq_recalled', 'areq_moved']) await backdate('sys_approval_request', id); |
| 342 | + |
| 343 | + const svc = new ApprovalService({ engine: engine as any, logger: quiet as any }); |
| 344 | + const source = slotLabelSourceOf(svc); |
| 345 | + let left: Record<string, any> | undefined; |
| 346 | + // The labeller is asked once the request page has been read and before any |
| 347 | + // of its rows is written — the window. The live writes land there, as the |
| 348 | + // sweep's do: plain system writes that stamp now. |
| 349 | + const racing = { |
| 350 | + slotLabels: async (slots: readonly string[]) => { |
| 351 | + if (!left) { |
| 352 | + await engine.update('sys_approval_request', { |
| 353 | + id: 'areq_recalled', status: 'recalled', pending_approvers: null, pending_approver_labels: null, |
| 354 | + completed_at: '2026-10-01T00:00:00.000Z', |
| 355 | + }, { context: SYSTEM } as any); |
| 356 | + await engine.update('sys_approval_request', { |
| 357 | + id: 'areq_moved', pending_approvers: 'u_mail', pending_approver_labels: 'Mia Mail', |
| 358 | + }, { context: SYSTEM } as any); |
| 359 | + left = { |
| 360 | + recalled: await row('sys_approval_request', 'areq_recalled'), |
| 361 | + moved: await row('sys_approval_request', 'areq_moved'), |
| 362 | + }; |
| 363 | + } |
| 364 | + return source.slotLabels(slots); |
| 365 | + }, |
| 366 | + }; |
| 367 | + await backfillApprovalDisplayLabels(engine as any, racing); |
| 368 | + |
| 369 | + const pick = (r: any) => ({ |
| 370 | + status: r.status, pending_approvers: r.pending_approvers, |
| 371 | + pending_approver_labels: r.pending_approver_labels, updated_at: r.updated_at, |
| 372 | + }); |
| 373 | + expect(left!.recalled.updated_at).not.toBe(BACKDATED); |
| 374 | + expect(pick(await row('sys_approval_request', 'areq_recalled'))).toEqual(pick(left!.recalled)); |
| 375 | + expect(pick(await row('sys_approval_request', 'areq_moved'))).toEqual(pick(left!.moved)); |
| 376 | + // Their null label columns are still filled, under the stamp the live write left. |
| 377 | + expect((await row('sys_approval_request', 'areq_recalled')).process_label).toBe('Deal Review'); |
| 378 | + |
| 379 | + // The untouched rows on the page are filled, their stamps kept. |
| 380 | + expect(await row('sys_approval_request', 'areq_open')).toMatchObject({ |
| 381 | + pending_approver_labels: 'Alice, position:legal', updated_at: BACKDATED, |
| 382 | + }); |
| 383 | + expect(await row('sys_approval_request', 'areq_done')).toMatchObject({ process_label: 'Deal Review', updated_at: BACKDATED }); |
| 384 | + }); |
323 | 385 | }); |
0 commit comments