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

fathom-attribution.test.ts

Language TypeScript14.1 KB308 lines

apps/dashboard/test/fathom-attribution.test.ts

  1. /**
  2. * Fathom attribution, pinned against the shapes the live API sends.
  3. *
  4. * Every label and address in this file has the shape of a real
  5. * `GET /meetings?include_action_items=true` response. Each case below is one
  6. * the frozen seed does not exercise:
  7. *
  8. * 1. A roster person must never sit in EXTERNAL_LABELS, or their own action
  9. * items resolve to "external" and reach nobody's view.
  10. * 2. `Marco Ibáñez Meridian Display` arrives with a null email. The
  11. * leading-token fallback splits on | - @ and that string has none of them,
  12. * so without a name-map entry it resolves to `unknown`.
  13. * 3. Unowned has two shapes: the bare "Meridian Display" and a fully null
  14. * `{name: null, email: null}`. Both mean nobody was named.
  15. * 4. Two people arrive under a second address the roster does not carry:
  16. * nora@ beside nora.l@, theo@ beside frontdesk@.
  17. *
  18. * The through-line: a display name is a guess and an email is a key.
  19. */
  20. import { describe, expect, it } from 'vitest'
  21. import {
  22. attributeFathomRef,
  23. isUnownedRef,
  24. participantsOf,
  25. EXTERNAL_LABELS,
  26. KNOWN_NON_PERSON_ADDRESSES,
  27. ROUTED_ADDRESSES,
  28. } from '../../../config/fathom-people.js'
  29. describe('email beats the display name', () => {
  30. it('resolves projects@ to Ines even though the name says "Lena K"', () => {
  31. // The case that justifies the order. An invitee with name "Lena K Meridian
  32. // Display" on projects@meridiandisplay.example, which the roster maps to Ines.
  33. // Matching the address settles it; matching the name would mean deciding
  34. // whether "Lena K" is Ines, which this code does not get to decide.
  35. const got = attributeFathomRef({ name: 'Lena K Meridian Display', email: 'projects@meridiandisplay.example' })
  36. expect(got.kind).toBe('roster')
  37. if (got.kind === 'roster') expect(got.person.name).toBe('Ines Carvalho')
  38. })
  39. it('is case-insensitive about the address', () => {
  40. const got = attributeFathomRef({ name: null, email: 'HELLO@MeridianDisplay.example' })
  41. expect(got.kind).toBe('roster')
  42. if (got.kind === 'roster') expect(got.person.name).toBe('Sam Whitaker')
  43. })
  44. })
  45. describe('the owner is on the roster, not external', () => {
  46. it.each([
  47. ['nora.l@meridiandisplay.example', 'Nora Lindqvist @ Meridian Display'],
  48. ['nora@meridiandisplay.example', 'Nora L'],
  49. ])('resolves %s to the roster', (email, name) => {
  50. // Both addresses appeared live on the same day. Only nora.l@ is on the
  51. // roster, so nora@ has to come from the Fathom alias table.
  52. const got = attributeFathomRef({ name, email })
  53. expect(got.kind).toBe('roster')
  54. if (got.kind === 'roster') expect(got.person.name).toBe('Nora Lindqvist')
  55. })
  56. it('resolves her by label alone when the email is null', () => {
  57. const got = attributeFathomRef({ name: 'Nora L', email: null })
  58. expect(got.kind).toBe('roster')
  59. })
  60. it('no longer lists her as an external label', () => {
  61. for (const label of ['nora l', 'nora lindqvist', 'nora lindqvist @ meridian display']) {
  62. expect(EXTERNAL_LABELS.has(label)).toBe(false)
  63. }
  64. })
  65. })
  66. describe('null-email labels still land on a person', () => {
  67. it('attributes "Marco Ibáñez Meridian Display" with no email', () => {
  68. // The exact live string. Before the fix this was `unknown`.
  69. const got = attributeFathomRef({ name: 'Marco Ibáñez Meridian Display', email: null })
  70. expect(got.kind).toBe('roster')
  71. if (got.kind === 'roster') expect(got.person.name).toBe('Marco Ibáñez')
  72. })
  73. it.each([
  74. ['Rosa Galván', 'Rosa Galván'],
  75. ['Rosa', 'Rosa Galván'],
  76. ['Theo | MeridianDisplay', 'Theo Bassett'],
  77. ])('attributes %j with no email', (label, expected) => {
  78. const got = attributeFathomRef({ name: label, email: null })
  79. expect(got.kind).toBe('roster')
  80. if (got.kind === 'roster') expect(got.person.name).toBe(expected)
  81. })
  82. it('still maps a departed person’s label, and lands off the roster', () => {
  83. // Hazel has left and Rosa holds her seat. Her spellings stay in
  84. // config/fathom-people.ts on purpose: her meetings are in Fathom forever,
  85. // and a recording that resolved to a person yesterday must not resolve to
  86. // a bare label today. The destination is a name the roster no longer
  87. // carries, and that is the honest answer, not a bug to paper over.
  88. const got = attributeFathomRef({ name: 'Hazel O', email: null })
  89. expect(got.kind).not.toBe('roster')
  90. })
  91. it("keeps Theo's second address on the roster", () => {
  92. const got = attributeFathomRef({ name: 'Office MeridianDisplay', email: 'theo@meridiandisplay.example' })
  93. expect(got.kind).toBe('roster')
  94. if (got.kind === 'roster') expect(got.person.name).toBe('Theo Bassett')
  95. })
  96. })
  97. describe('unowned means nobody, in either shape', () => {
  98. it('treats the bare company name as unowned', () => {
  99. expect(isUnownedRef({ name: 'Meridian Display', email: null })).toBe(true)
  100. expect(attributeFathomRef({ name: 'Meridian Display', email: null }).kind).toBe('unowned')
  101. })
  102. it('treats a fully null assignee as unowned', () => {
  103. // Seen once live. A different shape, the same fact.
  104. expect(isUnownedRef({ name: null, email: null })).toBe(true)
  105. expect(isUnownedRef(null)).toBe(true)
  106. })
  107. it('does NOT treat a named person as unowned', () => {
  108. expect(isUnownedRef({ name: 'Hazel O', email: 'accounts@meridiandisplay.example' })).toBe(false)
  109. })
  110. it('never turns the company name into a person', () => {
  111. // "Meridian Display" as a designer would be the Plinth `--` bug in another costume.
  112. const got = attributeFathomRef({ name: 'Meridian Display', email: null })
  113. expect(got.kind).not.toBe('roster')
  114. })
  115. })
  116. describe('an outsider holding an action item is recognised, not hand-listed', () => {
  117. // A prospect held one action item in the live 14-day window and is in no
  118. // map. Adding every client contact to EXTERNAL_LABELS by hand builds in the
  119. // lag every hand-kept table has, so Fathom's own is_external flag on the
  120. // meeting's invitee list settles it instead.
  121. const invitees = [
  122. { name: 'Peter Lang', email: 'peter@example.com', is_external: true },
  123. { name: 'Sam Whitaker - Meridian Display', email: 'hello@meridiandisplay.example', is_external: false },
  124. ]
  125. it('classifies them external using the invitee flag', () => {
  126. const got = attributeFathomRef({ name: 'Peter Lang', email: 'peter@example.com' }, invitees)
  127. expect(got.kind).toBe('external')
  128. })
  129. it('matches on the display name when the assignee has no email', () => {
  130. const got = attributeFathomRef({ name: 'Peter Lang', email: null }, invitees)
  131. expect(got.kind).toBe('external')
  132. })
  133. it('stays unknown without the invitee list, rather than guessing', () => {
  134. // No evidence, no verdict. Surfaced every run.
  135. expect(attributeFathomRef({ name: 'Peter Lang', email: null }).kind).toBe('unknown')
  136. })
  137. it('never lets the flag override a roster match', () => {
  138. // A roster person mis-flagged external must still be themselves; the email
  139. // path returns before any of this.
  140. const got = attributeFathomRef(
  141. { name: 'Sam Whitaker - Meridian Display', email: 'hello@meridiandisplay.example' },
  142. [{ name: 'Sam Whitaker - Meridian Display', email: 'hello@meridiandisplay.example', is_external: true }],
  143. )
  144. expect(got.kind).toBe('roster')
  145. })
  146. })
  147. describe('a routed mailbox sends its tasks to a person without putting them in the room', () => {
  148. // exec@meridiandisplay.example: the executive-assistant mailbox, with no
  149. // owner at present. Its tasks route to Sam, as a holding decision.
  150. const EA = { name: 'EA', email: 'exec@meridiandisplay.example' }
  151. it('is declared as routed, not as a person', () => {
  152. expect(ROUTED_ADDRESSES['exec@meridiandisplay.example']).toBe('Sam Whitaker')
  153. })
  154. it('attributes an action item assigned to the mailbox to Sam', () => {
  155. const got = attributeFathomRef(EA)
  156. expect(got.kind).toBe('roster')
  157. if (got.kind === 'roster') expect(got.person.name).toBe('Sam Whitaker')
  158. })
  159. it('does NOT make Sam a participant of a meeting the mailbox attended', () => {
  160. // The point: routing an item is not being in the room. If this fails, a
  161. // routing decision has silently become a visibility grant.
  162. const { roster, acknowledged, unmatched } = participantsOf([
  163. { ...EA, is_external: false },
  164. { name: 'Hazel O', email: 'accounts@meridiandisplay.example', is_external: false },
  165. ])
  166. // An invitee list from when accounts@ was Hazel's desk; Rosa holds it now,
  167. // so the address resolves to her. Fathom records an address
  168. // and nothing else, so a historical call cannot be told apart from a
  169. // current one: a known imprecision, written down beside the mapping in
  170. // config/fathom-people.ts rather than hidden by editing the fixture.
  171. expect(roster.map((p) => p.name)).toEqual(['Rosa Galván'])
  172. expect(roster.map((p) => p.name)).not.toContain('Sam Whitaker')
  173. expect(acknowledged).toEqual(['EA'])
  174. expect(unmatched).toEqual([])
  175. })
  176. it('never lets a routing entry override a real person\'s own address', () => {
  177. // Defensive: routing is checked after the roster lookups, so even a
  178. // mistaken entry for a real address could not re-route that person's items.
  179. const got = attributeFathomRef({ name: 'Hazel O', email: 'accounts@meridiandisplay.example' })
  180. expect(got.kind).toBe('roster')
  181. // The address wins over the label, and the address is Rosa's now; see
  182. // the note two tests above.
  183. if (got.kind === 'roster') expect(got.person.name).toBe('Rosa Galván')
  184. })
  185. })
  186. describe('participants decide who may see a meeting', () => {
  187. // The live invitee list of an internal leadership call, verbatim.
  188. const leadership = [
  189. { name: 'Nora Lindqvist @ Meridian Display', email: 'nora.l@meridiandisplay.example', is_external: false },
  190. { name: 'Office MeridianDisplay', email: 'frontdesk@meridiandisplay.example', is_external: false },
  191. { name: 'Lena K Meridian Display', email: 'projects@meridiandisplay.example', is_external: false },
  192. { name: 'Hazel O', email: 'accounts@meridiandisplay.example', is_external: false },
  193. { name: 'Sam Whitaker - Meridian Display', email: 'hello@meridiandisplay.example', is_external: false },
  194. ]
  195. it('resolves every internal invitee on a real leadership call', () => {
  196. const { roster, external, unmatched } = participantsOf(leadership)
  197. expect(roster.map((p) => p.name).sort()).toEqual([
  198. 'Ines Carvalho',
  199. 'Nora Lindqvist',
  200. // accounts@ on this call was Hazel; the seat is Rosa's now and the
  201. // address is all Fathom gives us.
  202. 'Rosa Galván',
  203. 'Sam Whitaker',
  204. 'Theo Bassett',
  205. ])
  206. expect(external).toEqual([])
  207. expect(unmatched).toEqual([])
  208. })
  209. it('EXCLUDES people who were not in the room', () => {
  210. // The whole point of the feature: Marco and Rafael were not on that call,
  211. // so they must not appear as viewers of it.
  212. const names = participantsOf(leadership).roster.map((p) => p.name)
  213. expect(names).not.toContain('Marco Ibáñez')
  214. expect(names).not.toContain('Rafael Montero')
  215. })
  216. it('surfaces an internal address that matches nobody instead of dropping it', () => {
  217. // Guessing who one is would be a silent fallback; dropping it would hide someone who
  218. // can see a meeting the board believes has no such viewer. `unmatched` is
  219. // meant to be empty, so anything in it is a question for a human.
  220. const { roster, unmatched } = participantsOf([
  221. { name: 'Someone New', email: 'newhire@meridiandisplay.example', is_external: false },
  222. ])
  223. expect(roster).toEqual([])
  224. expect(unmatched).toEqual(['Someone New'])
  225. })
  226. it('does NOT make a role alias a viewer', () => {
  227. // director@ is in KNOWN_NON_PERSON_ADDRESSES: it never appears without a
  228. // Nora address beside it, owns no action items, and
  229. // carries a role-shaped display name. Mapping it to her would gain no
  230. // visibility she lacks and would hand her access to whoever the alias covers
  231. // next: a permission widening with nobody editing the access list.
  232. const { roster, unmatched, acknowledged } = participantsOf([
  233. { name: 'Director MeridianDisplay', email: 'director@meridiandisplay.example', is_external: false },
  234. ])
  235. expect(roster).toEqual([])
  236. expect(acknowledged).toEqual(['Director MeridianDisplay'])
  237. // Known, so it is not an open question.
  238. expect(unmatched).toEqual([])
  239. })
  240. it('keeps the alias out of the roster even alongside real people', () => {
  241. const { roster, acknowledged } = participantsOf([
  242. { name: 'Director MeridianDisplay', email: 'director@meridiandisplay.example', is_external: false },
  243. { name: 'Nora Lindqvist', email: 'nora@meridiandisplay.example', is_external: false },
  244. ])
  245. expect(roster.map((p) => p.name)).toEqual(['Nora Lindqvist'])
  246. expect(acknowledged).toEqual(['Director MeridianDisplay'])
  247. })
  248. it('never resolves the alias to a person through attribution either', () => {
  249. // Belt and braces: the alias must not sneak in via the email path.
  250. expect(KNOWN_NON_PERSON_ADDRESSES.has('director@meridiandisplay.example')).toBe(true)
  251. const got = attributeFathomRef({ name: 'Director MeridianDisplay', email: 'director@meridiandisplay.example' })
  252. expect(got.kind).not.toBe('roster')
  253. })
  254. it('trusts is_external over any domain list', () => {
  255. // Northgate arrives under two domains, so a hand-kept table would miss one.
  256. const { roster, external } = participantsOf([
  257. { name: 'Rohan Mehta', email: 'rohan.mehta@northgate.example', is_external: true },
  258. { name: 'Priya Nair', email: 'priya.nair@northgate.example', is_external: true },
  259. { name: 'kc@clientco.example', email: 'kc@clientco.example', is_external: true },
  260. ])
  261. expect(roster).toEqual([])
  262. expect(external).toHaveLength(3)
  263. })
  264. it('does not double-count someone invited twice', () => {
  265. const { roster } = participantsOf([...leadership, leadership[0]!])
  266. expect(roster.filter((p) => p.name === 'Nora Lindqvist')).toHaveLength(1)
  267. })
  268. it('reports an empty invitee list as nobody, not as everybody', () => {
  269. // Fail closed: a meeting with no resolvable participants must expose itself
  270. // to no one rather than defaulting to the whole roster.
  271. expect(participantsOf([]).roster).toEqual([])
  272. expect(participantsOf(null).roster).toEqual([])
  273. })
  274. })