content-tracker.test.ts
Language TypeScript8.1 KB188 lines
/*** The Content Creation tracker as source five.** Most internal tasks live in this spreadsheet and nowhere else, while Ninety* holds a single rollup to-do for a dozen image jobs. Without this source both* designers render as nearly idle.** Every other source here is a system of record with* ids and timestamps. This one is a document people type into: owners are first* names, statuses are free text, deadlines are hand-typed in three formats, and* nothing reports when it was last edited. The tests below are about not* pretending otherwise.*/import { describe, expect, it } from 'vitest'import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'import { headline, peopleBoard, trackerCounts, type Snapshot } from '../src/collect.js'import { buildViewModel } from '../src/view-model.js'import { viewerFor } from '../src/viewer.js'import { MERIDIAN_PEOPLE, personFromTrackerOwner } from '../../../config/meridian-people.js'import { classifyTrackerStatus } from '../../../config/content-tracker.js'const DAY = '2026-08-12'const emailOf = (n: string) => MERIDIAN_PEOPLE.find((p) => p.name === n)!.email!const vmFor = (n: string, snap: Snapshot = FIXTURE_SNAPSHOT) =>buildViewModel(snap, headline(snap), [], DAY, viewerFor(emailOf(n)))const cardFor = (n: string, snap: Snapshot = FIXTURE_SNAPSHOT) =>vmFor(n, snap).myDays.find((d) => d.person.name === n)!describe('owners are first names, and that is only safe because it is checked', () => {it('resolves the spellings the live sheet actually uses', () => {for (const [owner, name] of [['Rafael', 'Rafael Montero'],['Marco', 'Marco Ibáñez'],['Ines', 'Ines Carvalho'],['Sam', 'Sam Whitaker'],['Theo', 'Theo Bassett'],['Nora', 'Nora Lindqvist'],]) {expect(personFromTrackerOwner(owner)?.name).toBe(name)}})it('has no two roster people sharing a first name', () => {// The day this stops being true, first-name matching starts attributing one// person's work to another and says nothing. config/meridian-people.ts throws at// load rather than letting that ship; this asserts the property directly so// the reason is findable from the test name.const firsts = MERIDIAN_PEOPLE.map((p) => p.name.split(' ')[0]!.toLowerCase())expect(new Set(firsts).size).toBe(firsts.length)})it('returns null for an owner nobody on the roster matches', () => {expect(personFromTrackerOwner('Freelancer')).toBeNull()expect(personFromTrackerOwner('')).toBeNull()})})describe('a blank Status is unclassified, never defaulted', () => {it('classifies the vocabulary the sheet uses and nothing else', () => {expect(classifyTrackerStatus('In Progress')).toBe('ours')expect(classifyTrackerStatus('Not Started')).toBe('ours')expect(classifyTrackerStatus('In Adjustments')).toBe('ours')expect(classifyTrackerStatus('Launched')).toBe('terminal')expect(classifyTrackerStatus('')).toBeNull()expect(classifyTrackerStatus('Something New')).toBeNull()})it('does not fold unclassified rows into open or done', () => {// Rule 2 in docs/ARCHITECTURE.md. `classify()` defaulted unknown lead stages// to `ours` "so they get noticed", nothing reported them, and most of the// pipeline became work the team owed. The counts must not add up without it.const t = trackerCounts(FIXTURE_SNAPSHOT)expect(t.unclassified).toBeGreaterThan(0)expect(t.ours + t.waiting + t.done + t.unclassified).toBe(t.rows)})it('names the unrecognised statuses on the page', () => {const vm = buildViewModel(FIXTURE_SNAPSHOT, headline(FIXTURE_SNAPSHOT), [], DAY)expect(vm.caveats.join(' ')).toMatch(/\(blank\)/)})})describe('"Ready to Launch" is context, not workload', () => {it('keeps it out of the load number', () => {// The design work is done and the launch is not. Counting it as backlog is// how a board makes someone look behind for something they delivered, for the// same reason waiting-on-client is not workload.const board = peopleBoard(FIXTURE_SNAPSHOT)const sam = board.rows.find((r) => r.name === 'Sam Whitaker')!expect(sam.sheetWaiting).toBeGreaterThan(0)const vm = vmFor('Sam Whitaker')const view = vm.people.find((p) => p.name === 'Sam Whitaker')!expect(view.load).toBe((sam.plinthOurMove ?? 0) + (sam.taskCount ?? 0) + (sam.todos ?? 0) + (sam.sheetOurMove ?? 0),)})it('still says it out loud in the aside', () => {const view = vmFor('Sam Whitaker').people.find((p) => p.name === 'Sam Whitaker')!expect(view.aside).toMatch(/ready to launch/i)})it('does not list it as a row on My Day', () => {const card = cardFor('Sam Whitaker')expect(card.items.map((i) => i.what)).not.toContain('New image for the feather flag back view')})})describe('tracker work reaches the person it belongs to', () => {it('puts rows on My Day and reconciles with the header', () => {for (const p of MERIDIAN_PEOPLE) {const d = cardFor(p.name)expect(d.ourMove + d.tasks + d.todos + d.tracker).toBe(d.person.load ?? 0)}})it('gives both designers the work Ninety only rolled up', () => {for (const name of ['Rafael Montero', 'Marco Ibáñez']) {expect(cardFor(name).tracker).toBeGreaterThan(0)}})it('shows a shared row to every named owner, and says who else holds it', () => {// "Nora & Sam" is one photoshoot and two people who have to show up.for (const name of ['Nora Lindqvist', 'Sam Whitaker']) {const row = cardFor(name).items.find((i) => i.what === 'Photo shoot for every feather flag size')expect(row, name).toBeDefined()expect(row!.meta).toMatch(/with /)}})it('does not give a tracker row a fabricated age', () => {// The sheet has no created date and its deadlines are free text, so there is// no number that means what an order's "34d" means. An invented one would// sort against real ages and win.for (const i of cardFor('Marco Ibáñez').items.filter((x) => x.meta.startsWith('Tracker'))) {expect(i.age).toBe('–')expect(i.late).toBe(false)}})})describe('the source row is honest about what it cannot know', () => {const LIVE: Snapshot = { ...FIXTURE_SNAPSHOT, mode: 'live', fixtureCapturedAt: null }const note = (snap: Snapshot = LIVE) =>buildViewModel(snap, headline(snap), [], DAY).sources.find((s) => s.name === 'Content Creation tracker',)!it('says staleness is not tracked rather than leaving it unsaid', () => {// The public CSV export sends no last-modified and the real modifiedTime// needs a credential the Worker does not have. The one input most likely to// go quietly stale is the one we can say the least about, so it says so.expect(note().note).toMatch(/not tracked/i)})it('does not call a hand-maintained sheet "live"', () => {// Every other pill means "we queried a system of record just now". Borrowing// the word here would make the least reliable input look like the most// current.expect(note().status).not.toMatch(/live/i)})it('reports a partial read as partial, and caveats every tracker number', () => {const partial: Snapshot = {...LIVE,sheetTabs: {ok: [{ label: 'Images', gid: '0', rows: 12 }],failed: [{ label: 'Newsletter', gid: '9', reason: 'HTTP 404' }],},}expect(note(partial).tone).toBe('degraded')expect(note(partial).note).toMatch(/PARTIAL/)const vm = buildViewModel(partial, headline(partial), [], DAY)expect(vm.caveats.join(' ')).toMatch(/FLOOR/)})it('renders a dead sheet as failed, never as nobody having tracker work', () => {const down: Snapshot = { ...LIVE, sheet: { ok: false, data: [], error: 'HTTP 403' } }const vm = buildViewModel(down, headline(down), [], DAY)expect(note(down).tone).toBe('failed')expect(vm.failures.join(' ')).toMatch(/403/)// Not zero: the column is absent, because nothing can see it.for (const r of peopleBoard(down).rows) expect(r.sheetOurMove).toBeNull()expect(cardFor('Marco Ibáñez', down).sourcesDown).toContain('the Content Creation tracker')})})