mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

bitrix-join.test.ts

Language TypeScript10.2 KB234 lines

apps/dashboard/test/bitrix-join.test.ts

  1. /**
  2. * The Bitrix task join.
  3. *
  4. * `Task.responsibleId` is Bitrix's numeric user id, stringified. Looking it up
  5. * as a roster display name (`personByName('1924')`) matches nothing and reads
  6. * "+ 0 tasks" on every row. A fixture that passed display names as
  7. * `responsibleId` would reproduce the shape that bug expects instead of the
  8. * shape Bitrix sends, and would pass: a fixture that agrees with a bug cannot
  9. * catch it.
  10. *
  11. * These tests pin the contract at both ends: ids are numeric, and the join
  12. * attributes a real number of them.
  13. */
  14. import { describe, expect, it } from 'vitest'
  15. import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'
  16. import { headline, peopleBoard, type Snapshot } from '../src/collect.js'
  17. import { buildViewModel } from '../src/view-model.js'
  18. import { viewerFor } from '../src/viewer.js'
  19. import {
  20. MERIDIAN_PEOPLE,
  21. offRosterBitrixIds,
  22. personFromBitrix,
  23. uncoveredByBitrix,
  24. unpinnedBitrixPeople,
  25. type Person,
  26. } from '../../../config/meridian-people.js'
  27. import { partitionTasks } from '@meridian/bitrix-client'
  28. describe('responsibleId is an id, and the fixture must say so', () => {
  29. it('carries only numeric responsibleIds, never a display name', () => {
  30. // Ids in the fixture are numeric, as Bitrix sends them.
  31. for (const t of FIXTURE_SNAPSHOT.tasks.data) {
  32. if (t.responsibleId === null) continue
  33. expect(t.responsibleId, `task ${t.id} "${t.title}"`).toMatch(/^\d+$/)
  34. }
  35. })
  36. it('never resolves a roster display name as a responsibleId', () => {
  37. for (const p of MERIDIAN_PEOPLE) expect(personFromBitrix(p.name)).toBeNull()
  38. })
  39. it('has a Bitrix id pinned for every roster person', () => {
  40. // An unpinned id silently zeroes that person's tasks, which is how this
  41. // started. unpinnedBitrixPeople() exists so it cannot go unnoticed again.
  42. expect(unpinnedBitrixPeople()).toEqual([])
  43. })
  44. })
  45. describe('the join actually attributes tasks', () => {
  46. const board = peopleBoard(FIXTURE_SNAPSHOT)
  47. it('attributes a non-zero number of real tasks', () => {
  48. // The bug was not a wrong count, it was zero. Assert the floor.
  49. expect(board.taskJoin.attributed).toBeGreaterThan(0)
  50. expect(board.taskJoin.real).toBeGreaterThan(board.taskJoin.attributed)
  51. })
  52. it('puts tasks on the specific people who hold them', () => {
  53. const by = new Map(board.rows.map((r) => [r.name, r.taskCount]))
  54. expect(by.get('Ines Carvalho')).toBe(2)
  55. expect(by.get('Theo Bassett')).toBe(1)
  56. expect(by.get('Marco Ibáñez')).toBe(1)
  57. expect(by.get('Sam Whitaker')).toBe(1)
  58. })
  59. it('gives her a measured zero now that her id is pinned, not Hazel’s task', () => {
  60. // With her own id pinned, Rosa's count is a real zero against this
  61. // fixture: she holds no open tasks in it, which is a fact rather than an
  62. // absence. Hazel's one task stays off her row, because the id did not
  63. // transfer (see the test below).
  64. const by = new Map(board.rows.map((r) => [r.name, r.taskCount]))
  65. expect(by.get('Rosa Galván')).toBe(0)
  66. })
  67. it('reports Hazel’s old Bitrix id as an off-roster holder rather than losing it', () => {
  68. // Her id was deliberately not moved to Rosa; it is an account, not a
  69. // seat. The task held by it does not vanish: it lands in offRoster, which
  70. // is "held by someone not on this roster" and is reported as such.
  71. expect(board.taskJoin.offRoster.map((o) => o.id)).toContain('2736')
  72. })
  73. it('reaches My Day, not just the board', () => {
  74. // peopleBoard and myDay resolved the join independently, so both had to be
  75. // fixed. Counting a task and showing it are different steps.
  76. const ines = MERIDIAN_PEOPLE.find((p) => p.name === 'Ines Carvalho')!
  77. const vm = buildViewModel(FIXTURE_SNAPSHOT, headline(FIXTURE_SNAPSHOT), [], '2026-08-12', viewerFor(ines.email))
  78. const card = vm.myDays.find((d) => d.person.name === 'Ines Carvalho')!
  79. expect(card.tasks).toBe(2)
  80. expect(card.items.filter((i) => i.meta.startsWith('Bitrix task'))).toHaveLength(2)
  81. })
  82. it('accounts for every real task: attributed + off-roster + unowned', () => {
  83. const { real } = partitionTasks(FIXTURE_SNAPSHOT.tasks.data)
  84. const unowned = real.filter((t) => !t.responsibleId).length
  85. const offRoster = board.taskJoin.offRoster.reduce((n, o) => n + o.tasks, 0)
  86. expect(board.taskJoin.attributed + offRoster + unowned).toBe(real.length)
  87. })
  88. })
  89. describe('an off-roster holder is reported, not chased and not hidden', () => {
  90. const board = peopleBoard(FIXTURE_SNAPSHOT)
  91. it('counts off-roster real tasks rather than absorbing them', () => {
  92. // Almost every Bitrix account is out of scope, so this is expected and is
  93. // not a coverage gap. But it is stated, because most of the
  94. // live real tasks sit here and the board deliberately shows none of them.
  95. expect(board.taskJoin.offRoster.length).toBeGreaterThan(0)
  96. for (const o of board.taskJoin.offRoster) expect(o.id).toMatch(/^\d+$/)
  97. })
  98. it('puts an off-roster task on nobody', () => {
  99. const vm = buildViewModel(FIXTURE_SNAPSHOT, headline(FIXTURE_SNAPSHOT), [], '2026-08-12')
  100. for (const d of vm.myDays) {
  101. expect(d.items.map((i) => i.what)).not.toContain('Fix the old quote form (April, 2024)')
  102. }
  103. })
  104. it('separates "held by someone off the roster" from "held by nobody"', () => {
  105. // Different facts. One is out of scope by decision; the other means nobody
  106. // has been asked to do it. Merging them loses the second entirely.
  107. const ids = board.taskJoin.offRoster.map((o) => o.id)
  108. expect(ids).not.toContain('')
  109. expect(ids.every(Boolean)).toBe(true)
  110. })
  111. it('does not count junk as off-roster', () => {
  112. // 20 junk rows are held by a departed person and a meeting room. Counting
  113. // them here would make an expected number look alarming.
  114. const offRoster = board.taskJoin.offRoster.reduce((n, o) => n + o.tasks, 0)
  115. expect(offRoster).toBeLessThan(board.taskJoin.junk)
  116. expect(board.taskJoin.offRoster.map((o) => o.id)).not.toContain('26')
  117. expect(board.taskJoin.offRoster.map((o) => o.id)).not.toContain('196')
  118. })
  119. })
  120. describe('two ways Bitrix can fail to see a person, and they are different', () => {
  121. // Synthetic rosters on purpose. Both states are transient (whoever is
  122. // mid-onboarding today is pinned tomorrow), so testing them against the live
  123. // roster means the test asserts nothing the moment the gap closes.
  124. const person = (over: Partial<Person>): Person => ({
  125. name: 'Test Person',
  126. role: 'Client Success',
  127. plinthAccounts: [],
  128. coveredBy: [],
  129. ...over,
  130. })
  131. it('separates "claims Bitrix, has no id" from "Bitrix does not cover them"', () => {
  132. // The first is a bug: the board says Bitrix covers them, so their tasks
  133. // render as a number, and the number is silently zero. The second is a
  134. // coverage fact: nothing claims to see them, so their count reads as not
  135. // tracked. Collapsing the two loses the bug.
  136. const claims = person({ name: 'Claims Coverage', coveredBy: ['bitrix-tasks'] })
  137. const uncovered = person({ name: 'Nobody Covers' })
  138. const pinned = person({ name: 'Pinned', bitrixUserId: 42, coveredBy: ['bitrix-tasks'] })
  139. expect(uncoveredByBitrix([claims, uncovered, pinned])).toEqual(['Nobody Covers'])
  140. })
  141. it('reports nobody as uncovered once their id is pinned', () => {
  142. const pinned = person({ name: 'Pinned', bitrixUserId: 42, coveredBy: ['bitrix-tasks'] })
  143. expect(uncoveredByBitrix([pinned])).toEqual([])
  144. })
  145. it('every roster person is now on one side or the other, with nobody in limbo', () => {
  146. // A person with an id who does not claim coverage would be an id nothing
  147. // uses; a person with neither is reported by uncoveredByBitrix. Both are
  148. // visible states; this asserts nobody is in the third, silent one.
  149. for (const p of MERIDIAN_PEOPLE) {
  150. const covered = p.coveredBy.includes('bitrix-tasks')
  151. expect(covered ? Boolean(p.bitrixUserId) : true, `${p.name}`).toBe(true)
  152. }
  153. })
  154. })
  155. describe('Data health states the count, not the method', () => {
  156. // Mode matters here. sourceStatuses() rewrites every pill on a fixture run, so
  157. // a sample board cannot claim live join numbers. That rewrite is deliberate
  158. // and is asserted elsewhere. The join note only exists on a live snapshot, so
  159. // these tests use one.
  160. const LIVE: Snapshot = { ...FIXTURE_SNAPSHOT, mode: 'live', fixtureCapturedAt: null }
  161. const noteFor = (snap: Snapshot): string => {
  162. const vm = buildViewModel(snap, headline(snap), [], '2026-08-12')
  163. return vm.sources.find((s) => s.name === 'Bitrix tasks')!.note
  164. }
  165. it('reports how many attributed, out of how many', () => {
  166. const board = peopleBoard(LIVE)
  167. const note = noteFor(LIVE)
  168. expect(note).toContain(`${board.taskJoin.attributed} of ${board.taskJoin.real}`)
  169. expect(note).toContain(String(board.taskJoin.junk))
  170. })
  171. it('counts the pinned ids rather than claiming a number', () => {
  172. // Counted, not asserted: "all roster ids are pinned" as a fixed sentence
  173. // would make a claim about somebody the page had never looked up.
  174. expect(noteFor(LIVE)).toMatch(/\d+ of \d+ roster ids are pinned/)
  175. expect(noteFor(LIVE)).not.toContain('All seven')
  176. })
  177. it('says off-roster holders are expected rather than a gap', () => {
  178. // The wording is not the assertion; the presence of the count is. An
  179. // off-roster number reported as a warning would be a permanent false alarm
  180. // about dozens of accounts nobody intends to show.
  181. expect(noteFor(LIVE)).toMatch(/outside the roster/i)
  182. })
  183. it('no longer claims the join works by describing how it matches', () => {
  184. // Numbers, not a description of the method: a description can read as
  185. // "fine" on a join that attributed nothing.
  186. expect(noteFor(LIVE)).not.toMatch(/by display name/i)
  187. })
  188. it('reports a dead source as dead rather than as a zero join', () => {
  189. const down: Snapshot = { ...LIVE, tasks: { ok: false, data: [], error: 'HTTP 401' } }
  190. const note = noteFor(down)
  191. expect(note).toContain('401')
  192. expect(note).not.toMatch(/0 of 0/)
  193. })
  194. })
  195. describe('offRosterBitrixIds', () => {
  196. it('groups by id and sorts by volume', () => {
  197. expect(offRosterBitrixIds(['26', '4', '26', '26', '4'])).toEqual([
  198. { id: '26', tasks: 3 },
  199. { id: '4', tasks: 2 },
  200. ])
  201. })
  202. it('ignores roster ids, nulls and blanks', () => {
  203. expect(offRosterBitrixIds(['1924', null, undefined, ' ', '7108'])).toEqual([])
  204. })
  205. })