t32-model-lifecycle.mjs 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. import assert from 'node:assert/strict'
  2. import { createPipeModelBatches } from '../../../src/modules/cockpit/services/pipeModelBatches.js'
  3. const pending = [], removed = [], changes = []
  4. const layer = createPipeModelBatches({
  5. load: async (group, events) => { const model = { key: group.key }; pending.push({ group, events, model }); return model },
  6. remove: (model) => removed.push(model.key),
  7. onChange: (state) => changes.push(state),
  8. })
  9. const group = { key: 'tile:valve', instances: [{ id: 'component:v' }] }
  10. await layer.sync([group])
  11. assert.deepEqual(layer.state().covered, [], 'await load is NOT model ready')
  12. pending[0].events.ready()
  13. assert.deepEqual(layer.state().covered, ['component:v'])
  14. await layer.sync([structuredClone(group)])
  15. assert.equal(pending.length, 1, 'unchanged input must not reload model')
  16. pending[0].events.error(new Error('late buffer error'))
  17. assert.deepEqual(layer.state().covered, [], 'late error restores fallback')
  18. assert.deepEqual(removed, ['tile:valve'])
  19. assert.equal(layer.state().failed, 1)
  20. await layer.sync([])
  21. assert.equal(layer.state().failed, 0)
  22. console.log('PASS readiness, immutable diff, late resource failure and clear')
  23. let finish
  24. const staleRemoved = []
  25. const stale = createPipeModelBatches({ load: async (_, events) => {
  26. await new Promise((resolve) => { finish = resolve })
  27. events.ready()
  28. return { id: 'stale' }
  29. }, remove: (model) => staleRemoved.push(model.id) })
  30. const waiting = stale.sync([group])
  31. await stale.clear()
  32. finish()
  33. await waiting
  34. assert.deepEqual(stale.state().covered, [])
  35. assert.deepEqual(staleRemoved, ['stale'])
  36. const splitEvents = []
  37. const split = createPipeModelBatches({ load: async (g, e) => { splitEvents.push(e); return {} }, remove: () => {} })
  38. await split.sync([{ key:'a', instances:[{id:'pipe:p'}] },{key:'b', instances:[{id:'pipe:p'}]}])
  39. splitEvents[0].ready()
  40. assert.deepEqual(split.state().covered, [], 'do not hide fallback until ALL path spans are ready')
  41. splitEvents[1].ready()
  42. assert.deepEqual(split.state().covered, ['pipe:p'])
  43. console.log('PASS stale completions cannot resurrect, split path waits for all spans')
  44. let clock = 0
  45. const attempts = []
  46. const retry = createPipeModelBatches({ now: () => clock, retryMs: 100, load: async (_, events) => {
  47. attempts.push(events)
  48. return {}
  49. }, remove: () => {} })
  50. await retry.sync([group])
  51. attempts[0].error()
  52. await retry.sync([group])
  53. assert.equal(attempts.length, 1, 'retry must respect cooldown')
  54. clock = 101
  55. await retry.sync([group])
  56. assert.equal(attempts.length, 2, 'same input recovers after temporary failure')
  57. attempts[1].ready()
  58. assert.deepEqual(retry.state().covered, ['component:v'])
  59. const stable = {key:'stable', instances:[{id:'component:s'}]}
  60. await retry.sync([group, stable])
  61. assert.equal(attempts.length, 3, 'adding one batch leaves ready batch intact')
  62. console.log('PASS failed batch retries with cooldown, incremental additions retain ready batches')