import { describe, expect, test } from 'vitest'; import type { PublishStep, PublishStepKind, PublishStepStatus } from './index.js'; import { applyBlockedByEligibility, buildPublishPlan, orderPublishSteps, rollUpPublishPlan } from './plan.js'; const TARGET = { applicationId: 'app-1', commitId: 'commit-1' }; function step(overrides: Partial & Pick): PublishStep { return { displayName: overrides.displayName ?? overrides.id, kind: overrides.kind ?? 'build', status: overrides.status ?? 'passed', ...overrides }; } describe('rollUpPublishPlan', () => { test('an empty plan is passed', () => { expect(rollUpPublishPlan([])).toBe('passed'); }); test('the worst status wins by precedence', () => { expect( rollUpPublishPlan([step({ id: 'a', status: 'passed' }), step({ id: 'b', status: 'advisory' }), step({ id: 'c', status: 'blocked' })]) ).toBe('blocked'); expect(rollUpPublishPlan([step({ id: 'a', status: 'in_progress' }), step({ id: 'b', status: 'failed' })])).toBe('failed'); expect(rollUpPublishPlan([step({ id: 'a', status: 'advisory' }), step({ id: 'b', status: 'passed' })])).toBe('advisory'); }); test('skipped never masks a real status', () => { expect(rollUpPublishPlan([step({ id: 'a', status: 'skipped' }), step({ id: 'b', status: 'passed' })])).toBe('passed'); expect(rollUpPublishPlan([step({ id: 'a', status: 'skipped' })])).toBe('skipped'); }); test('precedence order is exhaustive and worst-first', () => { const precedence: PublishStepStatus[] = [ 'failed', 'blocked', 'action_required', 'stale', 'in_progress', 'pending', 'advisory', 'passed', 'skipped' ]; for (let i = 0; i < precedence.length; i += 1) { const worse = precedence[i]; const better = precedence[i + 1]; const steps = better ? [step({ id: 'better', status: better }), step({ id: 'worse', status: worse })] : [step({ id: 'only', status: worse })]; expect(rollUpPublishPlan(steps)).toBe(worse); } }); }); describe('orderPublishSteps', () => { test('orders steps by the publish spine regardless of input order', () => { const ordered = orderPublishSteps([ step({ id: 'publish', kind: 'publish' }), step({ id: 'policy_gates:before_build', kind: 'policy_gates' }), step({ id: 'build', kind: 'build' }), step({ id: 'database_provisioning', kind: 'database_provisioning' }), step({ id: 'policy_gates:before_deploy', kind: 'policy_gates' }), step({ id: 'database_migration', kind: 'database_migration' }) ]); expect(ordered.map((s) => s.id)).toEqual([ 'policy_gates:before_build', 'build', 'policy_gates:before_deploy', 'database_provisioning', 'database_migration', 'publish' ]); }); test('omits ids that are not present', () => { const ordered = orderPublishSteps([step({ id: 'build', kind: 'build' }), step({ id: 'publish', kind: 'publish' })]); expect(ordered.map((s) => s.id)).toEqual(['build', 'publish']); }); }); describe('applyBlockedByEligibility', () => { test('marks later pending steps as blockedBy earlier unclear steps', () => { const steps = applyBlockedByEligibility([ step({ id: 'build', kind: 'build', status: 'in_progress' }), step({ id: 'publish', kind: 'publish', status: 'pending' }) ]); expect(steps[1].blockedBy).toEqual(['build']); expect(steps[1].status).toBe('pending'); }); test('does not overwrite a step that already failed', () => { const steps = applyBlockedByEligibility([ step({ id: 'build', kind: 'build', status: 'failed' }), step({ id: 'publish', kind: 'publish', status: 'failed' }) ]); expect(steps[1].blockedBy).toBeUndefined(); expect(steps[1].status).toBe('failed'); }); test('holds an in_progress publish as pending until earlier policy gates clear', () => { const steps = applyBlockedByEligibility([ step({ id: 'policy_gates:before_deploy', kind: 'policy_gates', status: 'in_progress', displayName: 'Pre-Publish Policies' }), step({ id: 'publish', kind: 'publish', status: 'in_progress', displayName: 'Publishing…' }) ]); expect(steps[1].status).toBe('pending'); expect(steps[1].blockedBy).toEqual(['policy_gates']); expect(steps[1].displayName).toBe('Publish'); }); test('clears blockedBy when all earlier steps are clear', () => { const steps = applyBlockedByEligibility([ step({ id: 'build', kind: 'build', status: 'passed' }), step({ id: 'publish', kind: 'publish', status: 'pending', blockedBy: ['build'] }) ]); expect(steps[1].blockedBy).toBeUndefined(); }); test('does not block policy gates on an unfinished build (they run concurrently)', () => { const steps = applyBlockedByEligibility([ step({ id: 'build', kind: 'build', status: 'in_progress' }), step({ id: 'policy_gates:before_deploy', kind: 'policy_gates', status: 'in_progress', displayName: 'Pre-Publish Policies' }) ]); expect(steps[1].blockedBy).toBeUndefined(); expect(steps[1].status).toBe('in_progress'); expect(steps[1].displayName).toBe('Pre-Publish Policies'); }); test('keeps a pending policy gate idle rather than waiting on the build', () => { const steps = applyBlockedByEligibility([ step({ id: 'build', kind: 'build', status: 'in_progress' }), step({ id: 'policy_gates:before_deploy', kind: 'policy_gates', status: 'pending' }) ]); expect(steps[1].blockedBy).toBeUndefined(); expect(steps[1].status).toBe('pending'); }); test('still blocks publish on both the build and policy gates', () => { const steps = applyBlockedByEligibility([ step({ id: 'build', kind: 'build', status: 'in_progress' }), step({ id: 'policy_gates:before_deploy', kind: 'policy_gates', status: 'in_progress' }), step({ id: 'publish', kind: 'publish', status: 'pending' }) ]); expect(steps[2].blockedBy).toEqual(['build', 'policy_gates']); expect(steps[2].status).toBe('pending'); }); test('lists each unclear kind at most once in blockedBy', () => { // Two earlier steps can share a kind (e.g. multiple database_* rows). The // rail waits on kinds, not step ids, so blockedBy must not repeat a kind. const steps = applyBlockedByEligibility([ step({ id: 'database_provisioning:a', kind: 'database_provisioning', status: 'in_progress' }), step({ id: 'database_provisioning:b', kind: 'database_provisioning', status: 'pending' }), step({ id: 'publish', kind: 'publish', status: 'pending' }) ]); expect(steps[2].blockedBy).toEqual(['database_provisioning']); }); }); describe('buildPublishPlan', () => { test('orders, applies eligibility, and rolls up', () => { const plan = buildPublishPlan(TARGET, [ step({ id: 'publish', kind: 'publish', status: 'pending' }), step({ id: 'build', kind: 'build', status: 'in_progress' }) ]); expect(plan.target).toEqual(TARGET); expect(plan.steps.map((s) => s.id)).toEqual(['build', 'publish']); expect(plan.steps[1].blockedBy).toEqual(['build']); expect(plan.status).toBe('in_progress'); }); test('an empty plan is passed', () => { expect(buildPublishPlan(TARGET, []).status).toBe('passed'); }); });