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

snapshot-file.test.ts

Language TypeScript9.4 KB228 lines

apps/dashboard/test/snapshot-file.test.ts

  1. /**
  2. * `pnpm morning` renders, writes the briefings from that render's numbers, then
  3. * renders again so the prose lands on the board. If the second render re-synced,
  4. * `syncedAt` would move, the briefings would no longer match the numbers beside
  5. * them, and every clean run would declare its own output stale.
  6. *
  7. * The staleness check is correct; the second render must reuse the same sync.
  8. * These tests guard that, and guard against "fixing" it by loosening the check
  9. * instead, which would remove the only thing telling a reader whether the two
  10. * halves of the page agree.
  11. */
  12. import { mkdtemp, writeFile, rm } from 'node:fs/promises'
  13. import { tmpdir } from 'node:os'
  14. import { join } from 'node:path'
  15. import { pathToFileURL } from 'node:url'
  16. import { afterAll, beforeAll, describe, expect, it } from 'vitest'
  17. import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'
  18. import { headline, type Snapshot } from '../src/collect.js'
  19. import { render } from '../src/render.js'
  20. import { readSnapshot, writeSnapshot } from '../src/snapshot-file.js'
  21. import type { Briefings } from '../src/briefings.js'
  22. let dir: string
  23. const at = (name: string) => pathToFileURL(join(dir, name))
  24. beforeAll(async () => {
  25. dir = await mkdtemp(join(tmpdir(), 'meridian-snap-'))
  26. })
  27. afterAll(async () => {
  28. await rm(dir, { recursive: true, force: true })
  29. })
  30. describe('the cached sync survives a round trip', () => {
  31. it('reads back what it wrote', async () => {
  32. const file = at('roundtrip.json')
  33. await writeSnapshot(FIXTURE_SNAPSHOT, file)
  34. const got = await readSnapshot(file)
  35. expect(got.ok).toBe(true)
  36. if (got.ok) expect(got.snap).toEqual(FIXTURE_SNAPSHOT)
  37. })
  38. it('preserves syncedAt exactly, which is the whole point', async () => {
  39. // If this drifts by even a millisecond the staleness check fires and the
  40. // page calls its own briefing stale, the bug this file is named for.
  41. const file = at('synced.json')
  42. await writeSnapshot(FIXTURE_SNAPSHOT, file)
  43. const got = await readSnapshot(file)
  44. expect(got.ok && got.snap.syncedAt).toBe(FIXTURE_SNAPSHOT.syncedAt)
  45. })
  46. it('keeps a failed source failed rather than turning it into an empty one', async () => {
  47. // The prime invariant, applied to the cache: a source that was down when the
  48. // sync ran must still read as down after a round trip, not as zero results.
  49. const broken: Snapshot = {
  50. ...FIXTURE_SNAPSHOT,
  51. leads: { ok: false, data: [], error: 'Bitrix returned 503' },
  52. }
  53. const file = at('broken.json')
  54. await writeSnapshot(broken, file)
  55. const got = await readSnapshot(file)
  56. expect(got.ok).toBe(true)
  57. if (got.ok) {
  58. expect(got.snap.leads.ok).toBe(false)
  59. expect(got.snap.leads.error).toBe('Bitrix returned 503')
  60. }
  61. })
  62. it('keeps a null apart from a zero', async () => {
  63. const file = at('nulls.json')
  64. await writeSnapshot({ ...FIXTURE_SNAPSHOT, ninetySpread: null }, file)
  65. const got = await readSnapshot(file)
  66. expect(got.ok && got.snap.ninetySpread).toBeNull()
  67. })
  68. it('carries fixture provenance across, so a cached demo cannot read as live', async () => {
  69. const file = at('mode.json')
  70. await writeSnapshot(FIXTURE_SNAPSHOT, file)
  71. const got = await readSnapshot(file)
  72. expect(got.ok && got.snap.mode).toBe('fixtures')
  73. })
  74. })
  75. describe('an unusable cache is refused, not rendered around', () => {
  76. it('says a missing file is missing', async () => {
  77. const got = await readSnapshot(at('nope.json'))
  78. expect(got).toMatchObject({ ok: false, missing: true })
  79. })
  80. it('distinguishes a bad file from an absent one', async () => {
  81. // Different problems, different fixes: one means run a sync, the other means
  82. // something wrote a bad file.
  83. const file = at('truncated.json')
  84. await writeFile(file, '{"syncedAt":"2026-', 'utf8')
  85. const got = await readSnapshot(file)
  86. expect(got).toMatchObject({ ok: false, missing: false })
  87. })
  88. it('refuses a snapshot with a source missing', async () => {
  89. const file = at('partial.json')
  90. const { leads, ...rest } = FIXTURE_SNAPSHOT
  91. void leads
  92. await writeFile(file, JSON.stringify(rest), 'utf8')
  93. const got = await readSnapshot(file)
  94. expect(got.ok).toBe(false)
  95. if (!got.ok) expect(got.reason).toMatch(/leads/)
  96. })
  97. it('refuses a snapshot with no syncedAt, because nothing on it can be dated', async () => {
  98. const file = at('undated.json')
  99. const { syncedAt, ...rest } = FIXTURE_SNAPSHOT
  100. void syncedAt
  101. await writeFile(file, JSON.stringify(rest), 'utf8')
  102. expect((await readSnapshot(file)).ok).toBe(false)
  103. })
  104. it('refuses a cache written before assignment coverage existed', async () => {
  105. // Same precedent as orderWindow above. A cache without assignment coverage
  106. // cannot say whether its per-person numbers were complete or blind, and
  107. // --render-only would put them on the page as though it could.
  108. const file = at('no-coverage.json')
  109. const { assignmentFields, ...window } = FIXTURE_SNAPSHOT.orderWindow
  110. void assignmentFields
  111. await writeFile(
  112. file,
  113. JSON.stringify({ ...FIXTURE_SNAPSHOT, orderWindow: window }),
  114. 'utf8',
  115. )
  116. const got = await readSnapshot(file)
  117. expect(got.ok).toBe(false)
  118. if (!got.ok) expect(got.reason).toMatch(/coverage|blind/)
  119. })
  120. it('refuses a cache written before per-key Fathom status existed', async () => {
  121. // Same precedent again, for the Fathom key list: neither default is safe. `[]` would render as "live,
  122. // from no keys", and leaving it undefined throws deep inside the view-model
  123. // instead of here, where the reason is actually known.
  124. const file = at('no-fathom-sources.json')
  125. const { fathomSources, ...rest } = FIXTURE_SNAPSHOT
  126. void fathomSources
  127. await writeFile(file, JSON.stringify(rest), 'utf8')
  128. const got = await readSnapshot(file)
  129. expect(got.ok).toBe(false)
  130. if (!got.ok) expect(got.reason).toMatch(/key|dark/i)
  131. })
  132. it('accepts a cache with no Fathom keys configured at all', async () => {
  133. // `fathomConfigured: false` is a legitimate state, not an old cache; the
  134. // guard above must not turn "nobody was asked" into an unreadable file.
  135. const file = at('fathom-unconfigured.json')
  136. const { fathomSources, ...rest } = FIXTURE_SNAPSHOT
  137. void fathomSources
  138. await writeFile(
  139. file,
  140. JSON.stringify({ ...rest, fathomConfigured: false, meetings: { ok: true, data: [], error: null } }),
  141. 'utf8',
  142. )
  143. expect((await readSnapshot(file)).ok).toBe(true)
  144. })
  145. it('refuses an unrecognised mode rather than assuming live', async () => {
  146. const file = at('mode-bad.json')
  147. await writeFile(file, JSON.stringify({ ...FIXTURE_SNAPSHOT, mode: 'probably-live' }), 'utf8')
  148. expect((await readSnapshot(file)).ok).toBe(false)
  149. })
  150. it('names a reason in every refusal', async () => {
  151. const file = at('empty.json')
  152. await writeFile(file, 'null', 'utf8')
  153. const got = await readSnapshot(file)
  154. expect(got.ok).toBe(false)
  155. if (!got.ok) expect(got.reason.length).toBeGreaterThan(0)
  156. })
  157. })
  158. describe('re-rendering the same sync clears the staleness warning honestly', () => {
  159. const snap = FIXTURE_SNAPSHOT
  160. const brief = (payloadGeneratedAt: string | null): Briefings => ({
  161. generatedAt: '2026-08-25T18:40:14+00:00',
  162. day: '2026-08-12',
  163. model: 'claude-opus-5',
  164. payloadGeneratedAt,
  165. mode: 'fixtures',
  166. // Both company variants: the default viewer is the company one, which reads
  167. // `company-allhands`. These tests are about staleness, not about who sees what.
  168. briefings: { company: 'The company brief.', 'company-allhands': 'The all-hands brief.' },
  169. })
  170. const page = (b: Briefings) => render(snap, headline(snap), [], '2026-08-12', b)
  171. /**
  172. * Either mode of the pairing caveat: same-day prose gets a note about scope and
  173. * earlier-day prose gets the re-run ask. The two negative assertions below
  174. * match either marker, because a negative assertion against a phrase no
  175. * longer emitted goes on passing while checking nothing.
  176. */
  177. const PAIRING_CAVEAT = /refresh every 15 minutes|pnpm morning/
  178. it('is silent when the prose and the numbers come from one sync', async () => {
  179. // `buildBriefingPayload` sets generatedAt to snap.syncedAt, so a brief
  180. // written from the cached sync matches it after a re-render.
  181. expect(page(brief(snap.syncedAt))).not.toMatch(PAIRING_CAVEAT)
  182. })
  183. it('still warns when they do not: the check is intact', () => {
  184. // The fix must not have worked by weakening this. If the sequence regresses,
  185. // or someone loosens briefingAge(), one of these two tests fails. An earlier
  186. // DAY is the mode that asks for something, because that one means the
  187. // morning brief never ran.
  188. expect(page(brief('2026-08-01T00:00:00.000Z'))).toMatch(/pnpm morning/)
  189. })
  190. it('says nothing either way when the briefing records no sync', () => {
  191. // Null is "no claim", not "matches". Asserting a match here would be the
  192. // check quietly passing on missing evidence.
  193. expect(page(brief(null))).not.toMatch(PAIRING_CAVEAT)
  194. })
  195. it('a re-render from cache produces the same page as the sync that wrote it', async () => {
  196. const file = at('rerender.json')
  197. await writeSnapshot(snap, file)
  198. const got = await readSnapshot(file)
  199. expect(got.ok).toBe(true)
  200. if (!got.ok) return
  201. const first = render(snap, headline(snap), [], '2026-08-12', brief(snap.syncedAt))
  202. const second = render(got.snap, headline(got.snap), [], '2026-08-12', brief(got.snap.syncedAt))
  203. expect(second).toBe(first)
  204. })
  205. })