import { describe, expect, it } from 'vitest' import { describeChange, isLabelChange, type IssueChange, planIssueChange, proposalsFor, summarizeChange, } from './apply-changes-v0.js' describe('planIssueChange', () => { it('swaps the estimate label, keeping non-axis labels', () => { const plan = planIssueChange(['est/2d', 'p/1', 'backend'], { kind: 'reestimate', issue: 1, estimate: 'est/5d' }) expect(plan.removed).toEqual(['est/2d']) expect(plan.added).toEqual(['est/5d']) expect(plan.labels).toEqual(['p/1', 'backend', 'est/5d']) expect(plan.noop).toBe(false) }) it('adds an estimate when none was set', () => { const plan = planIssueChange(['p/2'], { kind: 'reestimate', issue: 1, estimate: 'est/1d' }) expect(plan.removed).toEqual([]) expect(plan.added).toEqual(['est/1d']) expect(plan.labels).toEqual(['p/2', 'est/1d']) }) it('clears the axis when the target is null', () => { const plan = planIssueChange(['est/3d', 'p/1'], { kind: 'reprioritize', issue: 1, priority: null }) expect(plan.removed).toEqual(['p/1']) expect(plan.added).toEqual([]) expect(plan.labels).toEqual(['est/3d']) }) it('is a noop when the target already holds the axis alone', () => { const plan = planIssueChange(['est/2d', 'p/1'], { kind: 'reestimate', issue: 1, estimate: 'est/2d' }) expect(plan.noop).toBe(true) expect(plan.labels).toEqual(['p/1', 'est/2d']) }) it('cleans up a duplicated axis down to the target', () => { // two est/* labels — the change collapses to one const plan = planIssueChange(['est/2d', 'est/5d', 'p/1'], { kind: 'reestimate', issue: 1, estimate: 'est/5d' }) expect(plan.removed).toEqual(['est/2d']) expect(plan.added).toEqual([]) // est/5d already present expect(plan.labels).toEqual(['p/1', 'est/5d']) expect(plan.noop).toBe(false) }) it('reprioritize only touches the priority axis', () => { const plan = planIssueChange(['est/2d', 'p/3'], { kind: 'reprioritize', issue: 1, priority: 'p/1' }) expect(plan.labels).toEqual(['est/2d', 'p/1']) }) it('describeChange renders the diff', () => { const change: IssueChange = { kind: 'reestimate', issue: 1, estimate: 'est/5d' } expect(describeChange(planIssueChange(['est/2d'], change))).toBe('est/2d → est/5d') expect(describeChange(planIssueChange(['p/1'], { kind: 'reprioritize', issue: 1, priority: null }))).toBe('p/1 → ∅') expect(describeChange(planIssueChange(['est/5d'], change))).toBe('no change') }) }) describe('proposalsFor', () => { it('builds one proposal per changed axis, carrying the concrete change + diff', () => { const props = proposalsFor({ issue: 2, estimate: 'est/5d', priority: 'p/1' }, ['est/2d', 'p/3'], 'ChangeSource') expect(props).toHaveLength(2) expect(props[0].change).toEqual({ kind: 'reestimate', issue: 2, estimate: 'est/5d' }) expect(props[0].summary).toBe('est/2d → est/5d') expect(props[1].change).toEqual({ kind: 'reprioritize', issue: 2, priority: 'p/1' }) expect(props[0].issueTitle).toBe('ChangeSource') }) it('proposes an assignment only when it differs from the current assignee', () => { const change = proposalsFor({ issue: 2, assignee: 'christian' }, [], undefined, { currentAssignee: null }) expect(change).toHaveLength(1) expect(change[0].change).toEqual({ kind: 'assign', issue: 2, assignee: 'christian' }) expect(change[0].summary).toBe('assign → @christian') expect(change[0].plan).toBeUndefined() // already assigned → no proposal expect(proposalsFor({ issue: 2, assignee: 'christian' }, [], undefined, { currentAssignee: 'christian' })).toEqual([]) }) it('proposes a milestone move, labeled from the milestones list, skipping a no-op', () => { const ctx = { currentMilestoneId: 1, milestones: [{ id: 3, title: 'P5 — Dogfood' }] } const props = proposalsFor({ issue: 2, milestone: 3 }, [], undefined, ctx) expect(props).toHaveLength(1) expect(props[0].change).toEqual({ kind: 'remilestone', issue: 2, milestone: 3, milestoneTitle: 'P5 — Dogfood' }) expect(props[0].summary).toBe('milestone → P5 — Dogfood') // same milestone → no proposal expect(proposalsFor({ issue: 2, milestone: 1 }, [], undefined, ctx)).toEqual([]) }) it('drops a noop axis (already at the requested value)', () => { const props = proposalsFor({ issue: 2, estimate: 'est/2d', priority: 'p/1' }, ['est/2d', 'p/3']) expect(props.map((p) => p.change.kind)).toEqual(['reprioritize']) // estimate unchanged }) it('ignores invalid label values from the model', () => { const props = proposalsFor({ issue: 2, estimate: 'est/4d' as never, priority: 'high' as never }, []) expect(props).toEqual([]) }) it('returns nothing when no axis is provided', () => { expect(proposalsFor({ issue: 2 }, ['est/2d'])).toEqual([]) }) }) describe('unified change model (assign + milestone)', () => { it('isLabelChange narrows label kinds only', () => { expect(isLabelChange({ kind: 'reestimate', issue: 1, estimate: 'est/2d' })).toBe(true) expect(isLabelChange({ kind: 'reprioritize', issue: 1, priority: 'p/1' })).toBe(true) expect(isLabelChange({ kind: 'assign', issue: 1, assignee: 'christian' })).toBe(false) expect(isLabelChange({ kind: 'remilestone', issue: 1, milestone: 3 })).toBe(false) }) it('summarizeChange describes an assignment and an unassignment', () => { expect(summarizeChange({ kind: 'assign', issue: 1, assignee: 'christian' })).toBe('assign → @christian') expect(summarizeChange({ kind: 'assign', issue: 1, assignee: null })).toBe('unassign') }) it('summarizeChange describes a milestone set (by title) and removal', () => { expect(summarizeChange({ kind: 'remilestone', issue: 1, milestone: 3, milestoneTitle: 'P5 — Dogfood' })).toBe( 'milestone → P5 — Dogfood', ) expect(summarizeChange({ kind: 'remilestone', issue: 1, milestone: 3 })).toBe('milestone → #3') expect(summarizeChange({ kind: 'remilestone', issue: 1, milestone: null })).toBe('remove from milestone') }) it('summarizeChange delegates label kinds to describeChange (needs current labels)', () => { const change: IssueChange = { kind: 'reestimate', issue: 1, estimate: 'est/5d' } expect(summarizeChange(change, ['est/2d'])).toBe('est/2d → est/5d') expect(summarizeChange(change, ['est/5d'])).toBe('no change') }) })