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

briefing-payload.test.ts

Language TypeScript27.6 KB638 lines

apps/dashboard/test/briefing-payload.test.ts

  1. /**
  2. * The payload is a CONTRACT, not an output. Something downstream (a Python
  3. * analysis service, a local model, a person) reads it and writes sentences that
  4. * get shown to the team. These tests pin the properties that make those
  5. * sentences safe.
  6. *
  7. * The two that matter most:
  8. * 1. null never becomes 0. A dead source and an empty queue are opposite in
  9. * meaning and identical in shape.
  10. * 2. the interpretation rules ship INSIDE the payload. A consumer that has the
  11. * numbers but not the rules is more dangerous than one with no data.
  12. */
  13. import { describe, expect, it } from 'vitest'
  14. import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'
  15. import { orderQueue, orderQueueByDesigner, headline, todoLoad } from '../src/collect.js'
  16. import { queueView, sourceStatuses } from '../src/view-model.js'
  17. import {
  18. KNOWN_NON_ROSTER_NINETY,
  19. MERIDIAN_PEOPLE,
  20. personFromNinety,
  21. resolveNinety,
  22. unmappedNinetyOwners,
  23. } from '../../../config/meridian-people.js'
  24. import {
  25. buildBriefingPayload,
  26. EXEC_RULES,
  27. INTERPRETATION_RULES,
  28. LIST_CAP,
  29. PAYLOAD_SCHEMA_VERSION,
  30. UNMEASURABLE,
  31. } from '../src/briefing-payload.js'
  32. import { displayOrderNumber, isDisplayOrderNumber } from '@meridian/plinth-client'
  33. const snap = FIXTURE_SNAPSHOT
  34. describe('displayOrderNumber', () => {
  35. it('builds the number people actually say out loud', () => {
  36. expect(displayOrderNumber(39582, '2026-03-04T00:00:00Z')).toBe('MD2639582')
  37. })
  38. it('pads a single-digit year', () => {
  39. expect(displayOrderNumber(39582, '2009-03-04T00:00:00Z')).toBe('MD0939582')
  40. })
  41. it('returns the bare id rather than guessing a year', () => {
  42. // A wrong order number is worse than an obviously partial one: it looks
  43. // searchable and silently is not.
  44. expect(displayOrderNumber(39582, null)).toBe('39582')
  45. expect(isDisplayOrderNumber('39582')).toBe(false)
  46. })
  47. })
  48. describe('orderQueue', () => {
  49. const rows = orderQueue(snap.orderLines.data)
  50. it('only lists work this team owes', () => {
  51. // Waiting-on-client is a follow-up list, not a queue. Including it is how a
  52. // dashboard makes the team look behind when it is not.
  53. expect(rows.length).toBeGreaterThan(0)
  54. expect(rows.length).toBeLessThan(snap.orderLines.data.length)
  55. })
  56. it('gives every row a pasteable order number', () => {
  57. for (const r of rows) expect(isDisplayOrderNumber(r.orderNumber)).toBe(true)
  58. })
  59. it('sorts oldest first, because the top of the list is what is rotting', () => {
  60. const ages = rows.map((r) => r.ageDays ?? -1)
  61. expect([...ages].sort((a, b) => b - a)).toEqual(ages)
  62. })
  63. it('never echoes the order number back as a product name', () => {
  64. // An order-level export puts the order number in the title slot. Repeating
  65. // it in the column beside it is noise wearing the costume of data.
  66. for (const r of rows) {
  67. expect(r.product).not.toBe(r.orderNumber)
  68. expect(r.product).not.toBe(String(r.orderId))
  69. expect(r.product === null || r.product.length > 0).toBe(true)
  70. }
  71. })
  72. it('groups under the holder, with unassigned last', () => {
  73. const groups = orderQueueByDesigner(snap.orderLines.data)
  74. expect(groups.length).toBeGreaterThan(0)
  75. const nullAt = groups.findIndex((g) => g.designer === null)
  76. if (nullAt !== -1) expect(nullAt).toBe(groups.length - 1)
  77. expect(groups.flatMap((g) => g.rows)).toHaveLength(rows.length)
  78. })
  79. })
  80. describe('Ninety owners resolve through the roster, not by name equality', () => {
  81. // The failure this guards: Ninety spells names as someone typed them
  82. // ("Samuel Whitaker", "Marco Ibanez", "Rafael Monterro") against a roster
  83. // reading "Sam Whitaker", "Marco Ibáñez", "Rafael Montero". A name-equality
  84. // join matches nothing for those people, and their briefings report zero
  85. // to-dos, indistinguishable from being idle.
  86. //
  87. // Every ninetyId is pinned, so these tests assert the id join. What matters
  88. // is that every owner resolves, not how; a test that pinned a particular
  89. // workaround would make it read as a requirement.
  90. // The Ninety directory: id, and the spelling Ninety sends.
  91. const DIRECTORY: [string, string, string][] = [
  92. ['8326a8602530a97b5afd91ae', 'Nora Lindqvist', 'Nora Lindqvist'],
  93. ['6d8c1631dc0c3eda4c26b839', 'Theo Bassett', 'Theo Bassett'],
  94. ['4e77bc9877e856992de26b64', 'Ines Carvalho', 'Ines Carvalho'],
  95. ['3e894807d73d5a39b03fb411', 'Samuel Whitaker', 'Sam Whitaker'],
  96. ['8466ef547c9f9a6ee92fe8a0', 'Marco Ibanez', 'Marco Ibáñez'],
  97. ['05b7014d82e5b5ed5e68b309', 'Rafael Monterro', 'Rafael Montero'],
  98. ]
  99. it('resolves every id in the live directory to the right person', () => {
  100. for (const [id, ninetyName, rosterName] of DIRECTORY) {
  101. expect(personFromNinety(id, ninetyName)?.name, `${ninetyName} must resolve`).toBe(rosterName)
  102. }
  103. })
  104. it('resolves a DEPARTED person’s id to nobody, rather than to their successor', () => {
  105. // Hazel's Ninety account. Rosa took her mailbox, her Plinth assignment and
  106. // her seat, and deliberately not her Ninety id: that id is an account, and
  107. // reusing it would put Hazel's commitments on Rosa's briefing as though she
  108. // had agreed to them. Anything still open under it is held by nobody on
  109. // this roster; the fix for that is in Ninety.
  110. expect(personFromNinety('a568c7b30679efe19e6ed3a1', 'Hazel Okafor')).toBeNull()
  111. })
  112. it('resolves a drifted spelling by id, which is the point of pinning them', () => {
  113. // The name Ninety sends for these three does not match the roster; the id
  114. // does, so the spelling does not matter.
  115. expect(personFromNinety('3e894807d73d5a39b03fb411', 'Samuel Whitaker')?.name).toBe('Sam Whitaker')
  116. expect(personFromNinety('8466ef547c9f9a6ee92fe8a0', 'Marco Ibanez')?.name).toBe('Marco Ibáñez')
  117. expect(personFromNinety('05b7014d82e5b5ed5e68b309', 'Rafael Monterro')?.name).toBe('Rafael Montero')
  118. })
  119. it('survives a name Ninety has never sent before', () => {
  120. // The failure mode pinning exists to prevent: someone retypes a name in
  121. // Ninety and the person keeps resolving because the id did not change.
  122. expect(personFromNinety('8326a8602530a97b5afd91ae', 'V. Lindqvist')?.name).toBe('Nora Lindqvist')
  123. expect(personFromNinety('8326a8602530a97b5afd91ae', null)?.name).toBe('Nora Lindqvist')
  124. })
  125. it('prefers the id over the name when the two disagree', () => {
  126. // Ids are authoritative. A name that belongs to somebody else must not win.
  127. const match = personFromNinety('8326a8602530a97b5afd91ae', 'Sam Whitaker')
  128. expect(match?.name).toBe('Nora Lindqvist')
  129. })
  130. it('every person the board says Ninety COVERS carries an id', () => {
  131. // Scoped to people the board claims Ninety covers. A new hire genuinely
  132. // has no id until someone runs `pnpm roster` with credentials, and is
  133. // simply not claimed as covered (see the test below, and the coverageNote).
  134. // The property is that a covered person never resolves by spelling
  135. // instead of by id, since the spelling drifts and their to-dos would
  136. // silently read as zero.
  137. for (const p of MERIDIAN_PEOPLE.filter((x) => x.coveredBy.includes('ninety-todos'))) {
  138. expect(p.ninetyId, `${p.name} is marked ninety-todos with no ninetyId`).toBeTruthy()
  139. }
  140. })
  141. it('never claims Ninety coverage for somebody it cannot resolve', () => {
  142. // The prime invariant, applied to a person instead of a number. A new hire
  143. // with no id and `ninety-todos` in coveredBy would render as zero to-dos
  144. // (free capacity) when the truth is that no source can see them at all.
  145. for (const p of MERIDIAN_PEOPLE.filter((x) => !x.ninetyId)) {
  146. expect(p.coveredBy, `${p.name} has no ninetyId but claims Ninety`).not.toContain('ninety-todos')
  147. expect(p.coverageNote, `${p.name} has no ninetyId and no coverage note`).toBeTruthy()
  148. }
  149. })
  150. it('keeps no aliases, now that ids do the work', () => {
  151. // They were a fallback for exactly the gap that pinning closed. Left in
  152. // place they become a second, staler source of truth for the same question.
  153. for (const p of MERIDIAN_PEOPLE) {
  154. expect(p.aliases ?? [], `${p.name} still has aliases`).toEqual([])
  155. }
  156. })
  157. it('reports how a match was made, so a name-only match is not silent', () => {
  158. // A fallback standing in for knowledge must be surfaced. Every id is pinned,
  159. // so a name match means the id Ninety sent is one this roster has never
  160. // seen: attributed correctly today, and
  161. // silently attributed to nobody the moment the spelling also changes.
  162. expect(resolveNinety('8326a8602530a97b5afd91ae', 'Nora Lindqvist').via).toBe('id')
  163. expect(resolveNinety('an-id-nobody-pinned', 'Nora Lindqvist').via).toBe('name')
  164. expect(resolveNinety('an-id-nobody-pinned', 'Nobody At All').via).toBeNull()
  165. })
  166. it('returns null rather than guessing at an unknown person', () => {
  167. expect(personFromNinety(null, 'Someone Not On The Roster')).toBeNull()
  168. expect(personFromNinety(null, '')).toBeNull()
  169. expect(personFromNinety('an-id-nobody-pinned', null)).toBeNull()
  170. })
  171. it('reports unmapped owners instead of swallowing them', () => {
  172. // Non-empty means somebody's to-dos exist in the company total but appear on
  173. // no per-person view, and missing looks exactly like finished.
  174. const unmapped = unmappedNinetyOwners([
  175. { id: '3e894807d73d5a39b03fb411', name: 'Samuel Whitaker' },
  176. { id: 'u2', name: 'Someone Nobody Declared' },
  177. ])
  178. expect(unmapped).toHaveLength(1)
  179. expect(unmapped[0]!.name).toBe('Someone Nobody Declared')
  180. })
  181. it('does not flag people declared known-external', () => {
  182. // The Ninety directory is not the Meridian team: it holds former staff and
  183. // outside collaborators too. Grant Holloway is a former business coach;
  184. // undeclared, his absence from the roster reads as a coverage gap worth
  185. // chasing. Declaring it is the answer, written down.
  186. expect(unmappedNinetyOwners([{ id: '3609ad68b5259524e92470f5', name: 'Grant Holloway' }])).toEqual([])
  187. expect(KNOWN_NON_ROSTER_NINETY).toContain('Grant Holloway')
  188. })
  189. it('attributes every owner seen in the live sweep', () => {
  190. // Every owner must resolve: an unresolved one puts their share of company
  191. // work on no per-person view while it still counts in every company total.
  192. const owners = DIRECTORY.map(([id, name]) => ({ id, name }))
  193. expect(unmappedNinetyOwners(owners)).toEqual([])
  194. })
  195. it('surfaces unmapped owners in dataHealth', () => {
  196. const p = buildBriefingPayload(snap)
  197. expect(Array.isArray(p.dataHealth.unmappedTodoOwners)).toBe(true)
  198. })
  199. it('gives every to-do an ownerKey or an explicit null', () => {
  200. const p = buildBriefingPayload(snap)
  201. for (const t of p.people.flatMap((x) => x.todos)) {
  202. expect(t.ownerKey === null || typeof t.ownerKey === 'string').toBe(true)
  203. }
  204. })
  205. })
  206. describe('the all-hands brief cannot be built from Leadership to-dos', () => {
  207. /**
  208. * `brief.py`'s `without_restricted()` filters rows carrying `restricted`. If
  209. * `PayloadTodo` did not set the field, the filter would run, match nothing,
  210. * report `_restrictedTodosRemoved: 0`, and assemble the all-hands prompt from
  211. * every Leadership to-do title: fail-open on a confidentiality boundary, and
  212. * invisible from either side alone, since the TypeScript compiles and the
  213. * Python passes its own tests. These tests assert the two halves agree.
  214. */
  215. const payload = buildBriefingPayload(FIXTURE_SNAPSHOT)
  216. // Every list of to-do TITLES in the payload. `company.lateTodos` is a count,
  217. // not a list. Mistaking the two is what pointed the Python filter at nothing.
  218. const allTodos = [...payload.people.flatMap((p) => p.todos), ...payload.queues.lateTodos]
  219. it('marks every to-do as restricted or not, never leaves it undefined', () => {
  220. expect(allTodos.length).toBeGreaterThan(0)
  221. for (const t of allTodos) expect(typeof t.restricted).toBe('boolean')
  222. })
  223. it('marks Leadership Team to-dos restricted', () => {
  224. const hiring = allTodos.filter((t) => t.title.startsWith('Hire a client success associate'))
  225. expect(hiring.length).toBeGreaterThan(0)
  226. for (const t of hiring) expect(t.restricted).toBe(true)
  227. })
  228. it('marks a to-do with no team restricted, because it could be anything', () => {
  229. const untagged = allTodos.filter((t) => t.title === 'Coordinate backdrop images')
  230. expect(untagged.length).toBeGreaterThan(0)
  231. for (const t of untagged) expect(t.restricted).toBe(true)
  232. })
  233. it('leaves All Hands to-dos readable, so the all-hands brief is not empty', () => {
  234. // Failing closed on everything would be safe and useless: the rule is that
  235. // All Hands work stays visible so the whole team can see where the company
  236. // stands.
  237. const allHands = allTodos.filter((t) => t.title === 'Weekly product photo refresh')
  238. expect(allHands.length).toBeGreaterThan(0)
  239. for (const t of allHands) expect(t.restricted).toBe(false)
  240. })
  241. it('gives the all-hands filter something to actually remove', () => {
  242. // The assertion that would have caught the original bug: a payload where
  243. // nothing is restricted is indistinguishable from a broken flag.
  244. expect(allTodos.some((t) => t.restricted)).toBe(true)
  245. expect(allTodos.some((t) => !t.restricted)).toBe(true)
  246. })
  247. })
  248. describe('the leadership view reports capacity, never performance', () => {
  249. const p = buildBriefingPayload(snap)
  250. it('ships the exec rules separately so a consumer can put them first', () => {
  251. expect(p.execRules).toEqual(EXEC_RULES)
  252. expect(p.execRules.length).toBeGreaterThanOrEqual(6)
  253. })
  254. it('states plainly that speed and effort are not in the data', () => {
  255. const joined = p.execRules.join(' ').toLowerCase()
  256. for (const forbidden of ['slow', 'fast', 'behind', 'productive']) {
  257. expect(joined).toContain(forbidden) // named in order to be forbidden
  258. }
  259. expect(joined).toContain('cannot say anyone was')
  260. expect(joined).toContain('do not rank or compare people')
  261. })
  262. it('warns that age is not time on someone\'s desk', () => {
  263. // The single number most likely to be misread as a person's fault. Plinth
  264. // records no status-change timestamps, so this is a fact, not a hedge.
  265. expect(p.execRules.join(' ')).toMatch(/AGE IS NOT TIME ON SOMEONE'S DESK/)
  266. })
  267. it('carries the unmeasurable list, not just the measurable one', () => {
  268. expect(p.exec.unmeasurable).toEqual(UNMEASURABLE)
  269. const joined = p.exec.unmeasurable.join(' ').toLowerCase()
  270. expect(joined).toContain('no status-change timestamps')
  271. expect(joined).toContain('client reply latency is not recorded')
  272. expect(joined).toContain('phone calls, chats, walk-ups')
  273. })
  274. it('separates what the team owes from what is parked elsewhere', () => {
  275. // Only ourMove is team load. Rolling the others in is how a capacity view
  276. // starts describing a team as behind when it is not.
  277. expect(p.exec.ourMove).toBeGreaterThan(0)
  278. expect(p.exec.waitingOnClient).toBeGreaterThan(0)
  279. for (const l of p.exec.load) {
  280. if (l.holding !== null && l.parked !== null) {
  281. expect(l.holding + l.parked).toBeGreaterThanOrEqual(l.holding)
  282. }
  283. }
  284. })
  285. it('counts handoffs, because coordination is the small-team constraint', () => {
  286. expect(p.exec.handoffPoints).toBeGreaterThan(0)
  287. for (const l of p.exec.load) {
  288. if (l.handoffs !== null && l.holding !== null) {
  289. expect(l.handoffs).toBeLessThanOrEqual(l.holding)
  290. }
  291. }
  292. })
  293. it('gives team size as context for every count', () => {
  294. expect(p.exec.teamSize).toBe(p.people.length)
  295. })
  296. it('reports an uncovered person as null, never as an idle zero', () => {
  297. const uncovered = p.exec.load.filter((l) => l.holding === null)
  298. expect(uncovered.length).toBeGreaterThan(0)
  299. for (const l of uncovered) expect(l.holding).not.toBe(0)
  300. })
  301. it('exposes no rate, throughput, or completion FIELD', () => {
  302. // Checks field names, not prose: `unmeasurable` names throughput on
  303. // purpose, to rule it out. What must never appear is a per-person number
  304. // that could be read as a rate, because it will be reported as performance
  305. // within a week of existing.
  306. const banned = ['throughput', 'velocity', 'rate', 'perday', 'efficiency', 'completed', 'closed']
  307. const keys = new Set<string>()
  308. const walk = (v: unknown): void => {
  309. if (Array.isArray(v)) return v.forEach(walk)
  310. if (v && typeof v === 'object') {
  311. for (const [k, val] of Object.entries(v)) {
  312. keys.add(k.toLowerCase())
  313. walk(val)
  314. }
  315. }
  316. }
  317. walk({ ...p.exec, unmeasurable: undefined })
  318. for (const key of keys) {
  319. for (const b of banned) expect(key).not.toContain(b)
  320. }
  321. // And every per-person field is a count of things HELD, never a rate.
  322. for (const l of p.exec.load) {
  323. expect(Object.keys(l).sort()).toEqual(
  324. ['commitments', 'handoffs', 'holding', 'name', 'parked', 'role', 'shared'],
  325. )
  326. }
  327. })
  328. })
  329. describe('a fixture run never claims to be live', () => {
  330. // A fixture run must not render "live" pills over a frozen export: the board
  331. // that exists to stop sources asserting unearned status must not do it about
  332. // itself.
  333. it('marks every source as sample data, never live', () => {
  334. for (const s of sourceStatuses(snap)) {
  335. expect(s.status).toBe('sample data')
  336. expect(s.tone).not.toBe('live')
  337. }
  338. })
  339. it('says when the frozen data was captured', () => {
  340. expect(snap.mode).toBe('fixtures')
  341. expect(snap.fixtureCapturedAt).toBeTruthy()
  342. const ops = sourceStatuses(snap).find((s) => s.name === 'Plinth')!
  343. expect(ops.note).toContain('2026-08-09')
  344. expect(ops.note.toLowerCase()).toContain('already be shipped')
  345. })
  346. it('warns the model first, before any other rule', () => {
  347. // A model handed this with no marker writes a confident briefing about
  348. // orders that shipped two weeks ago and names people as holding finished
  349. // work. The warning has to be rule one, not a footnote.
  350. const p = buildBriefingPayload(snap)
  351. expect(p.mode).toBe('fixtures')
  352. expect(p.capturedAt).toBeTruthy()
  353. expect(p.rules[0]).toMatch(/SAMPLE DATA/)
  354. expect(p.rules.length).toBe(INTERPRETATION_RULES.length + 1)
  355. })
  356. it('a live snapshot carries no such warning', () => {
  357. const live = buildBriefingPayload({ ...snap, mode: 'live', fixtureCapturedAt: null })
  358. expect(live.mode).toBe('live')
  359. expect(live.capturedAt).toBeNull()
  360. expect(live.rules).toEqual(INTERPRETATION_RULES)
  361. })
  362. })
  363. describe('the fixture clock is pinned to the export, not to today', () => {
  364. // `placedAt` is frozen, so an age computed from Date.now() would grow one day
  365. // per real day, until every row tripped the aging threshold and a reviewer
  366. // learned to ignore the one flag that matters. A frozen snapshot must have
  367. // frozen ages.
  368. it('gives order 39511 the age it actually had, forever', () => {
  369. const line = snap.orderLines.data.find((l) => l.orderId === 39511)
  370. expect(line).toBeDefined()
  371. expect(line!.ageDays).toBe(30)
  372. })
  373. it('has no order older than the export window, however long the seed sits', () => {
  374. // Placed 01 Jul-07 Aug, exported 09 Aug. Nothing can legitimately exceed ~40
  375. // days. If this fails, something is measuring against a live clock again.
  376. for (const l of snap.orderLines.data) {
  377. if (l.ageDays !== null) expect(l.ageDays).toBeLessThan(45)
  378. }
  379. })
  380. })
  381. describe('no column echoes the column beside it', () => {
  382. // An order number echoed into productTitle and customer would make three
  383. // columns in a row say the same thing. Repetition reads as data.
  384. it('never renders the order number as a product or a client', () => {
  385. const q = queueView(snap)
  386. for (const g of q.groups) {
  387. for (const r of g.rows) {
  388. expect(r.product).not.toBe(r.orderNumber)
  389. expect(r.customer).not.toBe(r.orderNumber)
  390. }
  391. }
  392. })
  393. it('prints the absence as words, never as a blank cell', () => {
  394. const q = queueView(snap)
  395. for (const g of q.groups) {
  396. for (const r of g.rows) {
  397. expect(r.product.trim().length).toBeGreaterThan(0)
  398. expect(r.customer.trim().length).toBeGreaterThan(0)
  399. }
  400. }
  401. })
  402. })
  403. describe('an aging flag that fires on everything is suppressed', () => {
  404. it('replaces saturated badges with one sentence', () => {
  405. const q = queueView(snap)
  406. // The seed queue is genuinely mostly old, so this is the saturated path.
  407. expect(q.everythingIsOld).toBeTruthy()
  408. expect(q.everythingIsOld).toMatch(/median/)
  409. // and no row carries a badge, because a badge on everything is wallpaper.
  410. for (const g of q.groups) for (const r of g.rows) expect(r.aging).toBe(false)
  411. })
  412. it('names the unassigned group rather than leaving it blank', () => {
  413. const q = queueView(snap)
  414. const un = q.groups.find((g) => g.unassigned)
  415. if (un) expect(un.designer).toBe('Unassigned')
  416. for (const g of q.groups) expect(g.designer.trim().length).toBeGreaterThan(0)
  417. })
  418. })
  419. describe('headline treats a dead source as unknown, not empty', () => {
  420. it('reports commitments when Ninety is up', () => {
  421. const h = headline(snap)
  422. expect(h.commitments).toBeGreaterThan(0)
  423. expect(h.assignments).toBeGreaterThanOrEqual(h.commitments!)
  424. })
  425. it('reports null, NEVER 0, when Ninety is down', () => {
  426. const dead = {
  427. ...snap,
  428. ninety: { ok: false, data: [], error: '403 Forbidden' },
  429. ninetySpread: null,
  430. }
  431. const h = headline(dead)
  432. expect(h.commitments).toBeNull()
  433. expect(h.assignments).toBeNull()
  434. expect(h.lateTodos).toBeNull()
  435. // 0 would read as "everything is done". null reads as "we cannot see".
  436. expect(h.commitments).not.toBe(0)
  437. })
  438. })
  439. describe('todoLoad uses review gates, not raw overdue', () => {
  440. it('counts late only when a review has actually passed', () => {
  441. const loads = todoLoad(snap.ninety.data)
  442. for (const l of loads) expect(l.late).toBeLessThanOrEqual(l.open)
  443. expect(loads.some((l) => l.late > 0)).toBe(true)
  444. })
  445. })
  446. describe('buildBriefingPayload', () => {
  447. const p = buildBriefingPayload(snap)
  448. it('is versioned so a consumer can fail loudly on a breaking change', () => {
  449. expect(p.schemaVersion).toBe(PAYLOAD_SCHEMA_VERSION)
  450. expect(p.schemaVersion).toMatch(/^\d+\.\d+\.\d+$/)
  451. })
  452. it('ships the interpretation rules inside the payload', () => {
  453. // Fixture payloads prepend a sample-data warning, so this asserts every
  454. // standing rule survives rather than asserting an exact array.
  455. for (const rule of INTERPRETATION_RULES) expect(p.rules).toContain(rule)
  456. expect(p.rules.length).toBeGreaterThanOrEqual(6)
  457. })
  458. it('warns the consumer about the open-does-not-mean-unfinished trap', () => {
  459. // The single most dangerous sentence this system could generate is
  460. // "Sam is 15 days overdue" about work that is finished and awaiting
  461. // Monday sign-off. The rule that prevents it must be present.
  462. const joined = p.rules.join(' ').toLowerCase()
  463. expect(joined).toContain('does not mean unfinished')
  464. expect(joined).toContain('review')
  465. })
  466. it('warns the consumer not to conflate commitments with assignments', () => {
  467. expect(p.rules.join(' ').toLowerCase()).toContain('commitments and assignments are different')
  468. })
  469. it('stays small enough to run daily: this is the whole point of the seam', () => {
  470. // A model reasoning over raw payloads reads 200k-500k tokens per run. This
  471. // is the distilled version. If this assertion ever fails, the aggregation
  472. // has leaked back across the seam.
  473. const approxTokens = JSON.stringify(p).length / 4
  474. expect(approxTokens).toBeLessThan(12_000)
  475. })
  476. it('reports every source with its own ok flag and error', () => {
  477. expect(p.sources).toHaveLength(5)
  478. for (const s of p.sources) {
  479. expect(typeof s.ok).toBe('boolean')
  480. expect(s.covers.length).toBeGreaterThan(0)
  481. }
  482. expect(p.sources.map((s) => s.name)).toContain('Ninety to-dos')
  483. // With several keys, "some of the meetings were read" is a state the
  484. // payload can reach. Each person's `meetings` array is gated to what they
  485. // attended, so a short one looks completely normal. Without a source row there is nothing in the
  486. // payload that distinguishes a quiet fortnight from an unreadable key.
  487. expect(p.sources.map((s) => s.name)).toContain('Fathom meetings')
  488. })
  489. it('marks Fathom not-ok and says NOT ZERO when no key is configured', () => {
  490. const unset = buildBriefingPayload({ ...snap, fathomConfigured: false, fathomSources: [] })
  491. const fathom = unset.sources.find((s) => s.name === 'Fathom meetings')!
  492. expect(fathom.ok).toBe(false)
  493. expect(fathom.error).toMatch(/Not zero meetings/)
  494. for (const person of unset.people) expect(person.meetings).toBeNull()
  495. })
  496. it('marks Fathom not-ok when one key of three is dark, and names the owner', () => {
  497. // ok:false on a PARTIAL read is deliberate. The question this flag answers
  498. // for the model is "may I describe these meeting lists as complete", and
  499. // with a key dark the answer is no. brief.py turns it into a warning.
  500. const partial = buildBriefingPayload({
  501. ...snap,
  502. fathomSources: [
  503. { owner: 'Sam Whitaker', ok: true, error: null, returned: 8, truncated: false },
  504. { owner: 'Ines Carvalho', ok: false, error: 'HTTP 401', returned: 0, truncated: false },
  505. { owner: 'Theo Bassett', ok: true, error: null, returned: 2, truncated: false },
  506. ],
  507. })
  508. const fathom = partial.sources.find((s) => s.name === 'Fathom meetings')!
  509. expect(fathom.ok).toBe(false)
  510. expect(fathom.error).toContain('Ines Carvalho')
  511. expect(fathom.error).toContain('HTTP 401')
  512. // The working keys' meetings are still on the payload. A dark key degrades
  513. // the claim of completeness; it does not delete two other people's data.
  514. expect(partial.people.some((x) => (x.meetings?.length ?? 0) > 0)).toBe(true)
  515. })
  516. it('is ok and silent when every key read cleanly', () => {
  517. const fathom = p.sources.find((s) => s.name === 'Fathom meetings')!
  518. expect(fathom.ok).toBe(true)
  519. expect(fathom.error).toBeNull()
  520. })
  521. it('carries the failure reason through when a source dies', () => {
  522. const dead = buildBriefingPayload({
  523. ...snap,
  524. ninety: { ok: false, data: [], error: '403 Forbidden: Owner/Admin only' },
  525. ninetySpread: null,
  526. })
  527. const ninety = dead.sources.find((s) => s.name === 'Ninety to-dos')!
  528. expect(ninety.ok).toBe(false)
  529. expect(ninety.error).toContain('403')
  530. expect(dead.company.commitments).toBeNull()
  531. for (const person of dead.people) expect(person.todosOpen).toBeNull()
  532. })
  533. it('names the people carrying a shared commitment', () => {
  534. // Without this, the same item appears on three lists as if it were three
  535. // separate jobs and the briefing triple-counts the work.
  536. const shared = p.people.flatMap((x) => x.todos).find((t) => t.sharedWith.length > 0)
  537. expect(shared).toBeDefined()
  538. expect(shared!.sharedWith.length).toBeGreaterThanOrEqual(1)
  539. expect(shared!.sharedWith).not.toContain(shared!.owner)
  540. })
  541. it('distinguishes uncovered people from idle ones', () => {
  542. for (const person of p.people) {
  543. if (person.ordersOwed === null) expect(person.coverage.join(' ')).not.toMatch(/Plinth/i)
  544. }
  545. })
  546. it('caps lists and admits when it truncated', () => {
  547. for (const person of p.people) expect(person.orders.length).toBeLessThanOrEqual(LIST_CAP)
  548. expect(p.queues.oldestOrders.length).toBeLessThanOrEqual(LIST_CAP)
  549. expect(typeof p.queues.truncated).toBe('boolean')
  550. })
  551. it('counts orders with no designer as a data gap, not a queue', () => {
  552. expect(p.dataHealth.ordersMissingDesigner).toBe(p.queues.unassignedOrders.length)
  553. for (const o of p.queues.unassignedOrders) expect(o.designer).toBeNull()
  554. })
  555. it('groups on the Meridian calendar day, not UTC', () => {
  556. expect(p.timezone).toBe('America/Chicago')
  557. expect(p.day).toMatch(/^\d{4}-\d{2}-\d{2}$/)
  558. })
  559. it('never leaks raw HTML into a model context', () => {
  560. for (const t of p.people.flatMap((x) => x.todos)) {
  561. expect(t.title).not.toMatch(/<[a-z][^>]*>/i)
  562. }
  563. })
  564. })