assignments.test.ts
Language TypeScript7.9 KB200 lines
/*** Assignment arrives on the Plinth API as `production_owner` and* `account_owner` on orders.fetch. `normalizeAssignments` has two jobs:** The roster join. Plinth speaks in account labels ("Marco-Acct7-Desk"), the* board compares against roster names ("Marco Ibáñez"). Without this step* every `l.productionOwner === p.name` compares false and the whole team* renders as holding nothing.** The coverage guard. Assignment data that quietly stops arriving looks like* "nobody has much work" rather than like a break. Zero coverage must reach* the page as missing, never as zero.*/import { describe, expect, it } from 'vitest'import { normalizeAssignments, type Snapshot } from '../src/collect.js'import { sourceStatuses } from '../src/view-model.js'import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'import type { AssignmentFieldCoverage, OrderLine } from '@meridian/plinth-client'const line = (productionOwner: string | null, accountOwner: string | null = null): OrderLine =>({orderId: 24_001,ordersProductsId: 30_001,productId: 907,orderStatus: 'File Check',orderStatusId: 121,productStatus: 'File Check',productStatusId: 176,productTitle: 'Feather Banner',quantity: 1,price: 100,customer: 'Sample Customer',company: 'Sample Co',placedAt: '2026-08-20 09:00:00',amount: 100,vendor: null,userGroup: 'Retail',productionOwner,accountOwner,ageDays: 5,}) satisfies OrderLineconst full = (n: number): AssignmentFieldCoverage => ({production: n,accountOwner: n,total: n,})describe('Plinth account labels become roster names', () => {it('maps the labels seen on the live response', () => {// All four are real account labels. If Plinth adds an account,// the unmapped warning below is what surfaces it.const lines = [line('Marco-Acct7-Desk', 'Sam-Accounts-Desk'),line('Rafael-Prod-Desk', 'Hazel (Desk 2)'),]normalizeAssignments(lines, full(2))expect(lines.map((l) => l.productionOwner)).toEqual(['Marco Ibáñez', 'Rafael Montero'])// "Hazel (Desk 2)" is still the string Plinth sends, and it now resolves// to Rosa. The field name and value did not change when she took over the// role, so the orders assigned to it are now hers. This line is where that// decision actually takes effect: an order assigned months ago resolves to// whoever holds the account today, by design.expect(lines.map((l) => l.accountOwner)).toEqual(['Sam Whitaker', 'Rosa Galván'])})it('matches a label regardless of case', () => {const lines = [line('MARCO-ACCT7-DESK')]normalizeAssignments(lines, full(1))expect(lines[0]!.productionOwner).toBe('Marco Ibáñez')})it('leaves null alone rather than inventing an owner', () => {// A line Plinth sent as `--` arrives here already null. It must stay null: an// unassigned line is work nobody has picked up, and attributing it to// somebody would hide it from whoever should.const lines = [line(null, null)]const warnings = normalizeAssignments(lines, full(1))expect(lines[0]!.productionOwner).toBeNull()expect(lines[0]!.accountOwner).toBeNull()expect(warnings).toEqual([])})it('ROLE COMES FROM THE FIELD, never from the label text', () => {// "Marco-Acct7-Desk" is a designer and "Sam-Accounts-Desk" is Client// Success; the Accounts/Prod tokens in these strings mean nothing. Reading role// out of the label would swap the two of them.const lines = [line('Sam-Accounts-Desk', 'Marco-Acct7-Desk')]normalizeAssignments(lines, full(1))expect(lines[0]!.productionOwner).toBe('Sam Whitaker')expect(lines[0]!.accountOwner).toBe('Marco Ibáñez')})})describe('an unmapped account is surfaced, never rendered raw and never dropped', () => {it('warns once per label and still shows a readable name', () => {const lines = [line('Bob-New-Desk')]const warnings = normalizeAssignments(lines, full(1))expect(warnings).toHaveLength(1)expect(warnings[0]).toContain('Bob-New-Desk')expect(warnings[0]).toContain('plinth-people')// The default is surfaced, but the row still reads as a person rather than// going blank: a blank row loses the work.expect(lines[0]!.productionOwner).toBe('Bob')})it('says nothing about accounts that are mapped', () => {const lines = [line('Marco-Acct7-Desk', 'Sam-Accounts-Desk')]expect(normalizeAssignments(lines, full(1))).toEqual([])})})describe('field coverage decides whether the counts can be trusted', () => {it('reports absent fields as MISSING, not as zero work', () => {// The regression this exists for. If Plinth stops sending the fields, every line// reads unassigned, which looks exactly like a team with free capacity.const warnings = normalizeAssignments([], { production: 0, accountOwner: 0, total: 25 })const joined = warnings.join(' ')expect(joined).toMatch(/missing/i)expect(joined).toContain('production_owner')expect(joined).toContain('account_owner')})it('calls partial coverage a floor', () => {const warnings = normalizeAssignments([], { production: 10, accountOwner: 25, total: 25 })expect(warnings).toHaveLength(1)expect(warnings[0]).toContain('production_owner')expect(warnings[0]).toContain('10')expect(warnings[0]).toContain('25')expect(warnings[0]).toMatch(/floor/)})it('is silent at full coverage', () => {expect(normalizeAssignments([], full(25))).toEqual([])})it('is silent when no lines were fetched at all', () => {// Nothing to judge. A failed or empty sweep is reported by the source that// failed; saying "0 of 0 lines" here would describe one problem twice.expect(normalizeAssignments([], { production: 0, accountOwner: 0, total: 0 })).toEqual([])})})const liveSnap = (fields: AssignmentFieldCoverage, over: Partial<Snapshot> = {}): Snapshot => ({...FIXTURE_SNAPSHOT,mode: 'live',fixtureCapturedAt: null,orderWindow: { ...FIXTURE_SNAPSHOT.orderWindow, assignmentFields: fields },...over,})const assignmentPill = (snap: Snapshot) =>sourceStatuses(snap).find((s) => s.name === 'Plinth assignments')describe('the assignments pill tells the truth about coverage', () => {it('reads live when every line carried both fields', () => {const pill = assignmentPill(liveSnap(full(25)))expect(pill?.tone).toBe('live')expect(pill?.status).toBe('live')})it('reads FAILED, not empty, when the fields stop arriving', () => {const pill = assignmentPill(liveSnap({ production: 0, accountOwner: 0, total: 25 }))expect(pill?.tone).toBe('failed')expect(pill?.note).toMatch(/missing, not zero/)})it('degrades on partial coverage and says the counts are a floor', () => {const pill = assignmentPill(liveSnap({ production: 20, accountOwner: 25, total: 25 }))expect(pill?.tone).toBe('degraded')expect(pill?.note).toMatch(/floor/)})it('fails when the Plinth fetch itself failed, since assignments ride that request', () => {const pill = assignmentPill(liveSnap(full(0), {orderLines: { ok: false, data: [], error: 'HTTP 500' },}),)expect(pill?.tone).toBe('failed')expect(pill?.note).toContain('HTTP 500')})it('withholds a verdict when the window returned no lines', () => {// Zero lines is not evidence either way, and claiming "live" off an empty// window would assert something this run never observed.const pill = assignmentPill(liveSnap({ production: 0, accountOwner: 0, total: 0 }))expect(pill?.tone).toBe('degraded')expect(pill?.status).toBe('no lines')})it('never reads live on a fixture run', () => {// Same rule as every other pill: a frozen export must not borrow the// vocabulary of a live query.const pill = assignmentPill(FIXTURE_SNAPSHOT)expect(pill?.status).toBe('sample data')expect(pill?.tone).not.toBe('live')})})