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

assignments.test.ts

Language TypeScript7.9 KB200 lines

apps/dashboard/test/assignments.test.ts

  1. /**
  2. * Assignment arrives on the Plinth API as `production_owner` and
  3. * `account_owner` on orders.fetch. `normalizeAssignments` has two jobs:
  4. *
  5. * The roster join. Plinth speaks in account labels ("Marco-Acct7-Desk"), the
  6. * board compares against roster names ("Marco Ibáñez"). Without this step
  7. * every `l.productionOwner === p.name` compares false and the whole team
  8. * renders as holding nothing.
  9. *
  10. * The coverage guard. Assignment data that quietly stops arriving looks like
  11. * "nobody has much work" rather than like a break. Zero coverage must reach
  12. * the page as missing, never as zero.
  13. */
  14. import { describe, expect, it } from 'vitest'
  15. import { normalizeAssignments, type Snapshot } from '../src/collect.js'
  16. import { sourceStatuses } from '../src/view-model.js'
  17. import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'
  18. import type { AssignmentFieldCoverage, OrderLine } from '@meridian/plinth-client'
  19. const line = (productionOwner: string | null, accountOwner: string | null = null): OrderLine =>
  20. ({
  21. orderId: 24_001,
  22. ordersProductsId: 30_001,
  23. productId: 907,
  24. orderStatus: 'File Check',
  25. orderStatusId: 121,
  26. productStatus: 'File Check',
  27. productStatusId: 176,
  28. productTitle: 'Feather Banner',
  29. quantity: 1,
  30. price: 100,
  31. customer: 'Sample Customer',
  32. company: 'Sample Co',
  33. placedAt: '2026-08-20 09:00:00',
  34. amount: 100,
  35. vendor: null,
  36. userGroup: 'Retail',
  37. productionOwner,
  38. accountOwner,
  39. ageDays: 5,
  40. }) satisfies OrderLine
  41. const full = (n: number): AssignmentFieldCoverage => ({
  42. production: n,
  43. accountOwner: n,
  44. total: n,
  45. })
  46. describe('Plinth account labels become roster names', () => {
  47. it('maps the labels seen on the live response', () => {
  48. // All four are real account labels. If Plinth adds an account,
  49. // the unmapped warning below is what surfaces it.
  50. const lines = [
  51. line('Marco-Acct7-Desk', 'Sam-Accounts-Desk'),
  52. line('Rafael-Prod-Desk', 'Hazel (Desk 2)'),
  53. ]
  54. normalizeAssignments(lines, full(2))
  55. expect(lines.map((l) => l.productionOwner)).toEqual(['Marco Ibáñez', 'Rafael Montero'])
  56. // "Hazel (Desk 2)" is still the string Plinth sends, and it now resolves
  57. // to Rosa. The field name and value did not change when she took over the
  58. // role, so the orders assigned to it are now hers. This line is where that
  59. // decision actually takes effect: an order assigned months ago resolves to
  60. // whoever holds the account today, by design.
  61. expect(lines.map((l) => l.accountOwner)).toEqual(['Sam Whitaker', 'Rosa Galván'])
  62. })
  63. it('matches a label regardless of case', () => {
  64. const lines = [line('MARCO-ACCT7-DESK')]
  65. normalizeAssignments(lines, full(1))
  66. expect(lines[0]!.productionOwner).toBe('Marco Ibáñez')
  67. })
  68. it('leaves null alone rather than inventing an owner', () => {
  69. // A line Plinth sent as `--` arrives here already null. It must stay null: an
  70. // unassigned line is work nobody has picked up, and attributing it to
  71. // somebody would hide it from whoever should.
  72. const lines = [line(null, null)]
  73. const warnings = normalizeAssignments(lines, full(1))
  74. expect(lines[0]!.productionOwner).toBeNull()
  75. expect(lines[0]!.accountOwner).toBeNull()
  76. expect(warnings).toEqual([])
  77. })
  78. it('ROLE COMES FROM THE FIELD, never from the label text', () => {
  79. // "Marco-Acct7-Desk" is a designer and "Sam-Accounts-Desk" is Client
  80. // Success; the Accounts/Prod tokens in these strings mean nothing. Reading role
  81. // out of the label would swap the two of them.
  82. const lines = [line('Sam-Accounts-Desk', 'Marco-Acct7-Desk')]
  83. normalizeAssignments(lines, full(1))
  84. expect(lines[0]!.productionOwner).toBe('Sam Whitaker')
  85. expect(lines[0]!.accountOwner).toBe('Marco Ibáñez')
  86. })
  87. })
  88. describe('an unmapped account is surfaced, never rendered raw and never dropped', () => {
  89. it('warns once per label and still shows a readable name', () => {
  90. const lines = [line('Bob-New-Desk')]
  91. const warnings = normalizeAssignments(lines, full(1))
  92. expect(warnings).toHaveLength(1)
  93. expect(warnings[0]).toContain('Bob-New-Desk')
  94. expect(warnings[0]).toContain('plinth-people')
  95. // The default is surfaced, but the row still reads as a person rather than
  96. // going blank: a blank row loses the work.
  97. expect(lines[0]!.productionOwner).toBe('Bob')
  98. })
  99. it('says nothing about accounts that are mapped', () => {
  100. const lines = [line('Marco-Acct7-Desk', 'Sam-Accounts-Desk')]
  101. expect(normalizeAssignments(lines, full(1))).toEqual([])
  102. })
  103. })
  104. describe('field coverage decides whether the counts can be trusted', () => {
  105. it('reports absent fields as MISSING, not as zero work', () => {
  106. // The regression this exists for. If Plinth stops sending the fields, every line
  107. // reads unassigned, which looks exactly like a team with free capacity.
  108. const warnings = normalizeAssignments([], { production: 0, accountOwner: 0, total: 25 })
  109. const joined = warnings.join(' ')
  110. expect(joined).toMatch(/missing/i)
  111. expect(joined).toContain('production_owner')
  112. expect(joined).toContain('account_owner')
  113. })
  114. it('calls partial coverage a floor', () => {
  115. const warnings = normalizeAssignments([], { production: 10, accountOwner: 25, total: 25 })
  116. expect(warnings).toHaveLength(1)
  117. expect(warnings[0]).toContain('production_owner')
  118. expect(warnings[0]).toContain('10')
  119. expect(warnings[0]).toContain('25')
  120. expect(warnings[0]).toMatch(/floor/)
  121. })
  122. it('is silent at full coverage', () => {
  123. expect(normalizeAssignments([], full(25))).toEqual([])
  124. })
  125. it('is silent when no lines were fetched at all', () => {
  126. // Nothing to judge. A failed or empty sweep is reported by the source that
  127. // failed; saying "0 of 0 lines" here would describe one problem twice.
  128. expect(normalizeAssignments([], { production: 0, accountOwner: 0, total: 0 })).toEqual([])
  129. })
  130. })
  131. const liveSnap = (fields: AssignmentFieldCoverage, over: Partial<Snapshot> = {}): Snapshot => ({
  132. ...FIXTURE_SNAPSHOT,
  133. mode: 'live',
  134. fixtureCapturedAt: null,
  135. orderWindow: { ...FIXTURE_SNAPSHOT.orderWindow, assignmentFields: fields },
  136. ...over,
  137. })
  138. const assignmentPill = (snap: Snapshot) =>
  139. sourceStatuses(snap).find((s) => s.name === 'Plinth assignments')
  140. describe('the assignments pill tells the truth about coverage', () => {
  141. it('reads live when every line carried both fields', () => {
  142. const pill = assignmentPill(liveSnap(full(25)))
  143. expect(pill?.tone).toBe('live')
  144. expect(pill?.status).toBe('live')
  145. })
  146. it('reads FAILED, not empty, when the fields stop arriving', () => {
  147. const pill = assignmentPill(liveSnap({ production: 0, accountOwner: 0, total: 25 }))
  148. expect(pill?.tone).toBe('failed')
  149. expect(pill?.note).toMatch(/missing, not zero/)
  150. })
  151. it('degrades on partial coverage and says the counts are a floor', () => {
  152. const pill = assignmentPill(liveSnap({ production: 20, accountOwner: 25, total: 25 }))
  153. expect(pill?.tone).toBe('degraded')
  154. expect(pill?.note).toMatch(/floor/)
  155. })
  156. it('fails when the Plinth fetch itself failed, since assignments ride that request', () => {
  157. const pill = assignmentPill(
  158. liveSnap(full(0), {
  159. orderLines: { ok: false, data: [], error: 'HTTP 500' },
  160. }),
  161. )
  162. expect(pill?.tone).toBe('failed')
  163. expect(pill?.note).toContain('HTTP 500')
  164. })
  165. it('withholds a verdict when the window returned no lines', () => {
  166. // Zero lines is not evidence either way, and claiming "live" off an empty
  167. // window would assert something this run never observed.
  168. const pill = assignmentPill(liveSnap({ production: 0, accountOwner: 0, total: 0 }))
  169. expect(pill?.tone).toBe('degraded')
  170. expect(pill?.status).toBe('no lines')
  171. })
  172. it('never reads live on a fixture run', () => {
  173. // Same rule as every other pill: a frozen export must not borrow the
  174. // vocabulary of a live query.
  175. const pill = assignmentPill(FIXTURE_SNAPSHOT)
  176. expect(pill?.status).toBe('sample data')
  177. expect(pill?.tone).not.toBe('live')
  178. })
  179. })