155 lines
5.9 KiB
TypeScript
155 lines
5.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
|
import {
|
|
compare,
|
|
formatComparison,
|
|
toReport,
|
|
totalCost,
|
|
type Candidate,
|
|
type Comparison,
|
|
} from '../src/index.js'
|
|
|
|
const cmp: Comparison = { horizonYears: 3, dayRate: 800 }
|
|
|
|
const build = (p: Partial<Candidate> = {}): Candidate => ({
|
|
name: 'in-house build',
|
|
approach: 'build',
|
|
costs: { buildDays: 20, operateDaysPerYear: 2 },
|
|
where: 'decision-record.md',
|
|
...p,
|
|
})
|
|
|
|
describe('a prohibited option is not an expensive one', () => {
|
|
// A provider whose terms forbid the intended use is not a cheap option.
|
|
// No cost comparison can produce the right answer, because "buy" is not
|
|
// on the table.
|
|
const hostedProvider = build({
|
|
name: 'hosted provider',
|
|
approach: 'buy-hosted',
|
|
costs: { buildDays: 1, operateDaysPerYear: 0, cashPerYear: 600 },
|
|
blockers: [{ reason: 'terms of service prohibit the intended use', where: 'provider-terms.md' }],
|
|
})
|
|
|
|
it('disqualifies before any arithmetic', () => {
|
|
const r = compare([build(), hostedProvider], cmp)
|
|
expect(r.disqualified.map(c => c.name)).toEqual(['hosted provider'])
|
|
expect(r.viable.map(c => c.name)).toEqual(['in-house build'])
|
|
})
|
|
|
|
it('reports it as not-applicable, never as a costly option', () => {
|
|
const f = toReport([hostedProvider], cmp).findings[0]!
|
|
expect(f.determination).toBe('not-applicable')
|
|
expect(f.detail).toContain('pricing it implies it could win')
|
|
})
|
|
|
|
it('would otherwise have been the cheapest — which is the point', () => {
|
|
// 1 day + $600/yr = $2.6k, against build's $20.8k. Cost was never the
|
|
// deciding term.
|
|
const { blockers, ...unblocked } = hostedProvider
|
|
expect(totalCost(unblocked as Candidate, cmp)!).toBeLessThan(totalCost(build(), cmp)!)
|
|
})
|
|
|
|
it('cites the terms that disqualified it', () => {
|
|
expect(formatComparison([hostedProvider], cmp, { evidence: 'full' })).toContain('provider-terms.md')
|
|
})
|
|
})
|
|
|
|
describe('self-hosting a third-party tool is a build wearing a buy label', () => {
|
|
const selfHosted = build({
|
|
name: 'self-hosted tool',
|
|
approach: 'adopt-self-hosted',
|
|
costs: { buildDays: 3, operateDaysPerYear: 6 },
|
|
})
|
|
|
|
it('reports not-assessed when the operational surface is not enumerated', () => {
|
|
const f = toReport([selfHosted], cmp).findings[0]!
|
|
expect(f.determination).toBe('not-assessed')
|
|
expect(f.detail).toContain('taking on operating it')
|
|
expect(overall(toReport([selfHosted], cmp))).toBe('not-assessed')
|
|
})
|
|
|
|
it('excludes it from the ranking until it is enumerated', () => {
|
|
const r = compare([build(), selfHosted], cmp)
|
|
expect(r.uncostable.map(c => c.name)).toEqual(['self-hosted tool'])
|
|
expect(r.viable.map(c => c.name)).toEqual(['in-house build'])
|
|
})
|
|
|
|
it('costs it once the surface is listed', () => {
|
|
const listed = { ...selfHosted, operationalSurface: ['backups', 'upgrades', 'capacity', 'on-call'] }
|
|
const r = compare([build(), listed], cmp)
|
|
expect(r.uncostable).toHaveLength(0)
|
|
expect(r.viable).toHaveLength(2)
|
|
})
|
|
|
|
it('does not demand a surface from a hosted buy', () => {
|
|
const hosted = build({ name: 'hosted', approach: 'buy-hosted', costs: { buildDays: 1, operateDaysPerYear: 0, cashPerYear: 1200 } })
|
|
expect(toReport([hosted], cmp).findings[0]!.determination).toBe('pass')
|
|
})
|
|
})
|
|
|
|
describe('the operating term is the one nobody fills in', () => {
|
|
it('refuses to treat an unestimated operating cost as zero', () => {
|
|
// Treating it as free is exactly how "adopt" wins on paper.
|
|
const vague = build({ name: 'vague', costs: { buildDays: 5, operateDaysPerYear: null } })
|
|
expect(totalCost(vague, cmp)).toBeNull()
|
|
const f = toReport([vague], cmp).findings[0]!
|
|
expect(f.determination).toBe('not-assessed')
|
|
expect(f.detail).toContain('how "adopt" wins on paper')
|
|
})
|
|
|
|
it('costs over the horizon, not at a point', () => {
|
|
// 20 build days + 2 op days/yr x 3 yrs = 26 days = $20,800
|
|
expect(totalCost(build(), cmp)).toBe(20_800)
|
|
expect(totalCost(build(), { ...cmp, horizonYears: 1 })).toBe(17_600)
|
|
})
|
|
|
|
it('flags when most of the cost is operating', () => {
|
|
const opHeavy = build({ name: 'op-heavy', costs: { buildDays: 2, operateDaysPerYear: 10 } })
|
|
const f = toReport([opHeavy], cmp).findings[0]!
|
|
expect(f.summary).toContain('% of it operating')
|
|
expect(f.detail).toContain('paid every year and estimated never')
|
|
})
|
|
|
|
it('includes cash cost across the horizon', () => {
|
|
const paid = build({ costs: { buildDays: 0, operateDaysPerYear: 0, cashPerYear: 1000 } })
|
|
expect(totalCost(paid, cmp)).toBe(3000)
|
|
})
|
|
})
|
|
|
|
describe('recommendation', () => {
|
|
it('names the margin over the runner-up', () => {
|
|
const r = compare([
|
|
build(),
|
|
build({ name: 'cheap', costs: { buildDays: 1, operateDaysPerYear: 1 } }),
|
|
], cmp)
|
|
expect(r.winner!.name).toBe('cheap')
|
|
expect(r.reason).toContain('cheaper than in-house build')
|
|
})
|
|
|
|
it('calls the answer provisional while anything is unestimated', () => {
|
|
const r = compare([build(), build({ name: 'unknown', costs: { buildDays: null, operateDaysPerYear: null } })], cmp)
|
|
expect(r.reason).toContain('this is a provisional answer')
|
|
})
|
|
|
|
it('makes no recommendation when nothing can be costed', () => {
|
|
const r = compare([build({ costs: { buildDays: null, operateDaysPerYear: null } })], cmp)
|
|
expect(r.winner).toBeNull()
|
|
expect(r.reason).toContain('unestimated')
|
|
})
|
|
})
|
|
|
|
describe('conformance with the evidence-record standard', () => {
|
|
it('no candidates is not-assessed, and says why', () => {
|
|
expect(overall(toReport([], cmp))).toBe('not-assessed')
|
|
expect(formatComparison([], cmp))
|
|
.toContain('A decision with one option considered is not a comparison')
|
|
})
|
|
|
|
it('every conclusion carries a citation', () => {
|
|
expect(validateReport(toReport([
|
|
build(),
|
|
build({ name: 'x', approach: 'adopt-self-hosted', operationalSurface: ['backups'] }),
|
|
], cmp))).toEqual([])
|
|
})
|
|
})
|