Skip to content

src/status.test.ts

546 lines ยท 28.1 KB ยท TypeScript
Raw
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { makeLocator } from './input.ts'
import { dedupeChecks, normalizeContexts, type PrSnapshot } from './snapshot.ts'
import { analyzeDeploy, computeShip, type ComputeOptions } from './status.ts'
import type { Decision, ShipEvent } from './storage.ts'

const NOW = Date.parse('2026-10-05T20:00:00Z')
const HEAD = 'a'.repeat(40)

function snap(overrides: Partial<PrSnapshot> = {}): PrSnapshot {
	return {
		locator: makeLocator('kentcdodds', 'kody', 1),
		title: 'Test PR',
		state: 'OPEN',
		isDraft: false,
		merged: false,
		mergedAt: null,
		mergeCommitSha: null,
		mergeable: 'MERGEABLE',
		mergeStateStatus: 'CLEAN',
		reviewDecision: null,
		author: 'kentcdodds',
		baseRef: 'main',
		headRef: 'feature',
		headSha: HEAD,
		headCommittedDate: '2026-10-05T19:00:00Z',
		createdAt: '2026-10-05T19:00:30Z',
		readyForReviewAt: null,
		additions: 10,
		deletions: 2,
		checks: [],
		duplicateChecksHidden: 0,
		headRuns: [],
		threads: [],
		threadsTruncated: false,
		reviews: [],
		issueComments: [],
		deployRuns: null,
		fetchedAt: '2026-10-05T20:00:00Z',
		...overrides,
	}
}

function check(name: string, status: string, conclusion: string | null, extra: Record<string, unknown> = {}) {
	return normalizeContexts([{ __typename: 'CheckRun', name, status, conclusion, detailsUrl: 'https://github.com/kentcdodds/kody/actions/runs/1/job/2', startedAt: '2026-10-05T19:01:00Z', ...extra }])[0]
}

const bugbotDone = check('Cursor Bugbot', 'COMPLETED', 'SUCCESS', { checkSuite: { app: { slug: 'cursor' } } })

function opts(over: Partial<ComputeOptions> = {}): ComputeOptions {
	return { risk: 'medium', mergeAuthority: false, now: NOW, report: {}, expand: [], ...over }
}

function decision(kind: Decision['kind'], target: string, disposition: Decision['disposition'] = 'ignored'): Decision {
	return { prKey: 'kentcdodds/kody#1', kind, target, disposition, reason: 'because reasons', headSha: null, agentId: null, createdAt: '2026-10-05T19:30:00Z', expiresAt: '2026-10-12T19:30:00Z' }
}

test('dedupe prefers non-skipped twin', () => {
	const result = dedupeChecks([
		check('โœ… Validate', 'COMPLETED', 'SKIPPED'),
		check('โœ… Validate', 'COMPLETED', 'FAILURE', { startedAt: '2026-10-05T19:05:00Z' }),
	])
	assert.equal(result.checks.length, 1)
	assert.equal(result.checks[0].state, 'failure')
	assert.equal(result.hidden, 1)
})

test('failing CI focuses ci with details; decided check hides it', () => {
	const s = snap({ checks: [check('โœ… Validate', 'COMPLETED', 'FAILURE'), check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone] })
	const r = computeShip(s, [], [], opts())
	assert.equal(r.focus, 'ci')
	assert.equal(r.exit.status, 'needs_attention')
	const ci = r.remaining.find((x) => x.id === 'ci')!
	assert.ok(ci.details)
	assert.equal((ci.details!.failing as Array<unknown>).length, 1)
	// other steps carry no details (progressive disclosure)
	assert.equal(r.remaining.find((x) => x.id === 'merge')!.details, undefined)
	assert.ok(r.exampleInvokes.some((i) => i.command.includes('--local --code \'import run from "kody:@kentcdodds/ship-pr/decide"')))

	const r2 = computeShip(s, [decision('check', 'โœ… Validate')], [], opts())
	assert.equal(r2.done.some((d) => d.id === 'ci'), true)
	assert.equal(r2.focus, 'merge')
	assert.equal(r2.exit.status, 'ready_to_merge')
})

test('medium risk without Bugbot run โ†’ todo + trigger automation; low risk does not need it', () => {
	const s = snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS')] })
	const r = computeShip(s, [], [], opts())
	assert.equal(r.focus, 'ai-review')
	assert.equal(r.exit.status, 'action_available')
	assert.ok(r.automations.some((a) => a.id === 'trigger_bugbot'))
	const low = computeShip(s, [], [], opts({ risk: 'low' }))
	assert.equal(low.focus, 'merge')
	assert.ok(!low.automations.some((a) => a.id === 'trigger_bugbot'))
})

test('bugbot triggered event โ†’ waiting; non-kentcdodds author โ†’ unavailable (no wait)', () => {
	const s = snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS')] })
	const events: Array<ShipEvent> = [{ prKey: 'kentcdodds/kody#1', kind: 'bugbot_trigger', ref: HEAD, data: {}, createdAt: '2026-10-05T19:58:00Z', expiresAt: '2026-10-12T00:00:00Z' }]
	const r = computeShip(s, [], events, opts())
	assert.equal(r.exit.status, 'waiting')
	assert.equal(r.focus, 'ai-review')
	assert.equal(r.exit.pollAfterSeconds, 90)
	assert.ok(r.exit.wakeHints)
	assert.equal(r.exit.wakeHints?.requireTimerFallback, true)
	assert.equal(r.exit.wakeHints?.once, true)
	assert.equal(r.exit.wakeHints?.name, 'ship-pr-recheck-1')
	assert.ok((r.exit.wakeHints?.delaySeconds ?? 0) >= 90)
	assert.ok(r.exampleInvokes.some((i) => i.purpose.includes('timer fallback')))
	const devinPr = computeShip(snap({ author: 'devin-ai-integration[bot]', checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS')] }), [], [], opts())
	assert.equal(devinPr.focus, 'merge')
	assert.equal(devinPr.exit.wakeHints, undefined)
})

test('bugbot stalled does not auto re-trigger (would reset the stall clock)', () => {
	const s = snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS')] })
	const events: Array<ShipEvent> = [
		{
			prKey: 'kentcdodds/kody#1',
			kind: 'bugbot_trigger',
			ref: HEAD,
			data: {},
			createdAt: '2026-10-05T19:00:00Z',
			expiresAt: '2026-10-12T00:00:00Z',
		},
	]
	const r = computeShip(s, [], events, opts())
	assert.equal(r.focus, 'ai-review')
	assert.ok(r.exit.status === 'needs_attention' || r.exit.status === 'action_available')
	assert.ok(!r.automations.some((a) => a.id === 'trigger_bugbot'))
	const ai = r.remaining.find((x) => x.id === 'ai-review')
	assert.match(String(ai?.summary ?? ai?.details ?? ''), /stalled|Bugbot/i)
})

test('ci needs attention when no Validate-class checks appear after grace', () => {
	const onlyAi = computeShip(snap({ checks: [bugbotDone] }), [], [], opts())
	assert.equal(ciStep(onlyAi).status, 'needs_attention')
	assert.match(ciStep(onlyAi).summary, /no Validate-class CI/)
	const empty = computeShip(snap({ checks: [] }), [], [], opts())
	assert.equal(ciStep(empty).status, 'needs_attention')
})

test('review threads: valid bugbot finding must be addressed; addressing reply clears; decision hides', () => {
	const thread = (replies: Array<{ id: number; author: string; body: string }> = []) => ({
		id: 'PRRT_1',
		isResolved: false,
		isOutdated: false,
		path: 'src/a.ts',
		line: 3,
		comments: [
			{ id: 100, author: 'cursor[bot]', body: '### Null deref\n\n**High Severity**\n\nThis is a bug: crash when empty.', url: 'u', createdAt: 'x', replyToId: null },
			...replies.map((r) => ({ ...r, url: null, createdAt: 'y', replyToId: 100 })),
		],
	})
	const base = { checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone] }
	const r = computeShip(snap({ ...base, threads: [thread()] }), [], [], opts())
	assert.equal(r.focus, 'ai-review')
	const details = r.remaining.find((x) => x.id === 'ai-review')!.details as any
	assert.equal(details.sorted.mustAddress.length, 1)
	assert.equal(details.sorted.mustAddress[0].severity, 'high')
	assert.equal(details.sorted.mustAddress[0].title, 'Null deref')

	const fixed = computeShip(snap({ ...base, threads: [thread([{ id: 101, author: 'kody-bot', body: 'Fixed in abc1234def.' }])] }), [], [], opts())
	assert.equal(fixed.focus, 'merge')

	const decided = computeShip(snap({ ...base, threads: [thread()] }), [decision('thread', '100', 'wontfix')], [], opts())
	assert.equal(decided.focus, 'merge')
	assert.equal(decided.activeDecisions.length, 1)

	const resolved = computeShip(snap({ ...base, threads: [{ ...thread(), isResolved: true }] }), [], [], opts())
	assert.equal(resolved.focus, 'merge')
})

test('invalid bot finding offers reply automation and does not block', () => {
	const threads = [{ id: 'PRRT_2', isResolved: false, isOutdated: false, path: null, line: null, comments: [{ id: 200, author: 'devin-ai-integration[bot]', body: 'Nit: style-only rename.', url: null, createdAt: 'x', replyToId: null }] }]
	const r = computeShip(snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone], threads }), [], [], opts())
	assert.equal(r.focus, 'merge')
	assert.ok(r.automations.some((a) => a.id === 'reply_invalid' && a.commentId === 200))
})

test('high risk parks unless mergeAuthority; park decision moves to friction then report', () => {
	const s = snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone] })
	const r = computeShip(s, [], [], opts({ risk: 'high' }))
	assert.equal(r.focus, 'merge')
	assert.equal(r.exit.status, 'needs_attention')
	const ok = computeShip(s, [], [], opts({ risk: 'high', mergeAuthority: true }))
	assert.equal(ok.exit.status, 'ready_to_merge')
	const parked = computeShip(s, [decision('step', 'merge', 'skipped')], [], opts({ risk: 'high' }))
	assert.equal(parked.focus, 'friction')
	assert.equal(parked.exit.reportStatus, 'Parked')
	const reported = computeShip(s, [decision('step', 'merge', 'skipped'), decision('step', 'friction', 'skipped')], [], opts({ risk: 'high', report: { title: 'T' } }))
	assert.equal(reported.focus, 'report')
	const reportStep = reported.remaining.find((x) => x.id === 'report')!
	assert.deepEqual((reportStep.details as any).missing, ['agentId', 'difficulty', 'summary'])
	const briefReport = computeShip(s, [decision('step', 'merge', 'skipped'), decision('step', 'friction', 'skipped')], [], opts({ risk: 'high', brief: true }))
	const briefReportDetails = briefReport.remaining.find((x) => x.id === 'report')!.details as any
	assert.deepEqual(briefReportDetails.allowed.difficulty, ['Easy', 'Medium', 'Hard'])
	assert.deepEqual(briefReportDetails.allowed.status, ['Shipped', 'Parked', 'Blocked'])
	assert.equal(briefReportDetails.rules, undefined) // prose stripped in brief
	const briefFriction = computeShip(s, [decision('step', 'merge', 'skipped')], [], opts({ risk: 'high', brief: true }))
	const briefFrictionDetails = briefFriction.remaining.find((x) => x.id === 'friction')!.details as any
	assert.deepEqual(briefFrictionDetails.settle.dispositions, ['ignored', 'skipped', 'wontfix', 'accepted'])
	assert.equal(briefFrictionDetails.settle.via, 'decide')
	assert.equal(briefFrictionDetails.settle.target, 'friction')
	assert.deepEqual(briefFrictionDetails.allowed.disposition, ['ignored', 'skipped', 'wontfix', 'accepted'])
	assert.equal(briefFrictionDetails.bar, undefined) // prose stripped in brief
	const done = computeShip(s, [decision('step', 'merge', 'skipped'), decision('step', 'friction', 'skipped')], [{ prKey: 'kentcdodds/kody#1', kind: 'summary_sent', ref: 'discord', data: { status: 'Parked' }, createdAt: 'z', expiresAt: '2026-10-12T00:00:00Z' }], opts({ risk: 'high' }))
	assert.equal(done.exit.done, true)
	assert.equal(done.phase, 'done')
})

test('merged: deploy waits on running Validate then done; friction actionable meanwhile', () => {
	const merged = { state: 'MERGED' as const, merged: true, mergedAt: '2026-10-05T19:55:00Z', mergeCommitSha: 'b'.repeat(40) }
	const running = computeShip(
		snap({ ...merged, deployRuns: [{ id: 1, name: 'โœ… Validate', event: 'push', status: 'in_progress', conclusion: null, url: 'u', createdAt: 'x' }, { id: 2, name: '๐Ÿš€ Deploy (production)', event: 'workflow_run', status: 'completed', conclusion: 'skipped', url: 'u', createdAt: 'x' }] }),
		[],
		[],
		opts(),
	)
	assert.equal(running.remaining.find((x) => x.id === 'deploy')!.status, 'waiting')
	assert.equal(running.focus, 'friction')
	const ok = computeShip(
		snap({ ...merged, deployRuns: [{ id: 1, name: 'โœ… Validate', event: 'push', status: 'completed', conclusion: 'success', url: 'u', createdAt: 'x' }, { id: 3, name: '๐Ÿš€ Deploy (production)', event: 'workflow_run', status: 'completed', conclusion: 'success', url: 'u', createdAt: 'x' }] }),
		[decision('step', 'friction', 'accepted')],
		[],
		opts(),
	)
	assert.equal(ok.focus, 'report')
	assert.equal(ok.exit.reportStatus, 'Shipped')
})

test('expand adds details to non-focus steps', () => {
	const s = snap({ checks: [check('๐Ÿงช Node', 'IN_PROGRESS', null)] })
	const r = computeShip(s, [], [], opts({ risk: 'low', expand: ['merge'] }))
	assert.equal(r.focus, 'ci')
	assert.ok(r.remaining.find((x) => x.id === 'merge')!.details)
})

test('low risk: bot threads optional, human threads need attention', () => {
	const t = (author: string) => ({ id: 'PRRT_h', isResolved: false, isOutdated: false, path: null, line: null, comments: [{ id: 300, author, body: 'Please rename this.', url: null, createdAt: 'x', replyToId: null }] })
	const base = { checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS')] }
	const bot = computeShip(snap({ ...base, threads: [t('coderabbitai[bot]')] }), [], [], opts({ risk: 'low' }))
	assert.equal(bot.focus, 'merge')
	const human = computeShip(snap({ ...base, threads: [t('someone')] }), [], [], opts({ risk: 'low' }))
	assert.equal(human.focus, 'ai-review')
	assert.equal(human.exit.status, 'needs_attention')
})

test('reportStatus is Pending until merge or park', () => {
	const s = snap({ checks: [check('๐Ÿงช Node', 'IN_PROGRESS', null)] })
	assert.equal(computeShip(s, [], [], opts()).exit.reportStatus, 'Pending')
	const ready = computeShip(snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone] }), [], [], opts())
	assert.equal(ready.exit.status, 'ready_to_merge')
	assert.equal(ready.exit.reportStatus, 'Pending')
})

test('deploy summary names running runs with their urls', () => {
	const merged = { state: 'MERGED' as const, merged: true, mergedAt: '2026-10-05T19:55:00Z', mergeCommitSha: 'b'.repeat(40) }
	const r = computeShip(
		snap({ ...merged, locator: makeLocator('epicweb-dev', 'gratitext', 101), deployRuns: [{ id: 9, name: '๐Ÿš€ Deploy', event: 'push', status: 'in_progress', conclusion: null, url: 'https://github.com/epicweb-dev/gratitext/actions/runs/9', createdAt: 'x' }] }),
		[],
		[],
		opts({ expand: ['deploy'] }),
	)
	const deploy = r.remaining.find((x) => x.id === 'deploy')!
	assert.match(deploy.summary, /๐Ÿš€ Deploy \(https:\/\/github\.com\/epicweb-dev\/gratitext\/actions\/runs\/9\)/)
	assert.ok(((deploy.details as any).notes as Array<string>).some((n) => n.includes('No repo-specific deploy hints')))
})

test('brief drops checklist and prose; re-tick invokes ask for brief', () => {
	const s = snap({ checks: [check('โœ… Validate', 'COMPLETED', 'FAILURE'), check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone] })
	const full = computeShip(s, [], [], opts())
	assert.ok(full.checklist)
	assert.ok(full.exampleInvokes.some((i) => i.command.includes('ship-pr/tick') && i.command.includes('"brief":true')))
	const brief = computeShip(s, [], [], opts({ brief: true }))
	assert.equal(brief.checklist, undefined)
	const ci = brief.remaining.find((x) => x.id === 'ci')!
	assert.equal((ci.details as any).notes, undefined)
	assert.equal(((ci.details as any).failing as Array<unknown>).length, 1)
	assert.ok(JSON.stringify(brief).length < JSON.stringify(full).length)
})

function statusCtx(context: string, state: string, createdAt: string) {
	return normalizeContexts([{ __typename: 'StatusContext', context, state, description: 'Review in progress', targetUrl: null, createdAt }])[0]
}

function crReview(commitSha: string) {
	return { id: 77, author: 'coderabbitai[bot]', state: 'COMMENTED', body: 'Nitpick comments (1)', submittedAt: '2026-10-05T19:10:00Z', commitSha, url: 'https://github.com/kentcdodds/kody/pull/1#pullrequestreview-77' }
}

test('high risk: coderabbit review on head settles a stuck pending status', () => {
	const s = snap({
		mergeStateStatus: 'UNSTABLE',
		checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone, statusCtx('CodeRabbit', 'PENDING', '2026-10-05T19:55:00Z')],
		reviews: [crReview(HEAD)],
	})
	const r = computeShip(s, [], [], opts({ risk: 'high', mergeAuthority: true }))
	assert.equal(r.focus, 'merge')
	assert.match(r.remaining.find((x) => x.id === 'merge')!.summary, /unstable/)
})

test('high risk: coderabbit stuck pending after reviewing a prior head stops blocking after 15m', () => {
	const base = { checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone], reviews: [crReview('c'.repeat(40))] }
	const fresh = computeShip(snap({ ...base, checks: [...base.checks, statusCtx('CodeRabbit', 'PENDING', '2026-10-05T19:50:00Z')] }), [], [], opts({ risk: 'high', mergeAuthority: true }))
	assert.equal(fresh.focus, 'ai-review')
	assert.match(fresh.remaining.find((x) => x.id === 'ai-review')!.summary, /waiting on coderabbit/)
	const stuck = computeShip(snap({ ...base, checks: [...base.checks, statusCtx('CodeRabbit', 'PENDING', '2026-10-05T19:40:00Z')] }), [], [], opts({ risk: 'high', mergeAuthority: true, expand: ['ai-review'] }))
	assert.equal(stuck.focus, 'merge')
})

test('high risk: coderabbit pending with no review keeps waiting', () => {
	const s = snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone, statusCtx('CodeRabbit', 'PENDING', '2026-10-05T19:00:00Z')] })
	const r = computeShip(s, [], [], opts({ risk: 'high', mergeAuthority: true }))
	assert.equal(r.focus, 'ai-review')
})

function crEmptyReview(commitSha: string, id = 78) {
	return { id, author: 'coderabbitai[bot]', state: 'COMMENTED', body: '', submittedAt: '2026-10-05T19:12:00Z', commitSha, url: `https://github.com/kentcdodds/kody/pull/1#pullrequestreview-${id}` }
}

test('high risk: empty-body CodeRabbit thread-reply on head does not settle a pending status', () => {
	const s = snap({
		mergeStateStatus: 'UNSTABLE',
		checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone, statusCtx('CodeRabbit', 'PENDING', '2026-10-05T19:55:00Z')],
		reviews: [crEmptyReview(HEAD), crEmptyReview(HEAD, 79)],
	})
	const r = computeShip(s, [], [], opts({ risk: 'high', mergeAuthority: true, expand: ['ai-review'] }))
	assert.equal(r.focus, 'ai-review')
	assert.match(r.remaining.find((x) => x.id === 'ai-review')!.summary, /waiting on coderabbit/)
	const details = r.remaining.find((x) => x.id === 'ai-review')!.details as any
	assert.equal(details.reviewers.coderabbit.state, 'running')
})

test('high risk: whitespace-only CodeRabbit review on head is ignored like empty body', () => {
	const s = snap({
		checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone, statusCtx('CodeRabbit', 'PENDING', '2026-10-05T19:55:00Z')],
		reviews: [{ id: 80, author: 'coderabbitai[bot]', state: 'COMMENTED', body: '   \n\t  ', submittedAt: '2026-10-05T19:12:00Z', commitSha: HEAD, url: 'https://github.com/kentcdodds/kody/pull/1#pullrequestreview-80' }],
	})
	const r = computeShip(s, [], [], opts({ risk: 'high', mergeAuthority: true }))
	assert.equal(r.focus, 'ai-review')
})

test('high risk: empty-body on head plus earlier substantive review still waits while pending is fresh', () => {
	const s = snap({
		checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone, statusCtx('CodeRabbit', 'PENDING', '2026-10-05T19:50:00Z')],
		reviews: [crReview('c'.repeat(40)), crEmptyReview(HEAD)],
	})
	const r = computeShip(s, [], [], opts({ risk: 'high', mergeAuthority: true }))
	assert.equal(r.focus, 'ai-review')
	assert.match(r.remaining.find((x) => x.id === 'ai-review')!.summary, /waiting on coderabbit/)
})

const CR_OUTSIDE_FIXTURE = `**Actionable comments posted: 1**

> **โš ๏ธ Outside diff range comments (1)**
>
> <details>
> <summary><strong>๐ŸŸก Minor</strong> ยท Update the refresh-family retention description. ยท <code>authentication.md:791-793</code></summary><blockquote>
>
> \`docs/contributing/architecture/authentication.md:791-793\`
> **Update the refresh-family retention description.**
>
> This paragraph says account deletion does not sweep refresh-family keys.
>
> <!-- cr-comment:v1:9c172a7d1afd80bd5b22f105 -->
>
> </blockquote></details>

---

<!-- autofix_checkbox_start -->
`

test('outside-diff body findings land in mustAddress and decide hides them', () => {
	const base = { checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone] }
	const reviews = [
		{
			id: 5463345636,
			author: 'coderabbitai[bot]',
			state: 'COMMENTED',
			body: CR_OUTSIDE_FIXTURE,
			submittedAt: '2026-10-08T22:00:00Z',
			commitSha: HEAD,
			url: 'https://github.com/kentcdodds/kody/pull/1#pullrequestreview-5463345636',
		},
	]
	const r = computeShip(snap({ ...base, reviews }), [], [], opts({ expand: ['ai-review'] }))
	assert.equal(r.focus, 'ai-review')
	const details = r.remaining.find((x) => x.id === 'ai-review')!.details as any
	assert.equal(details.sorted.mustAddress.length, 1)
	const item = details.sorted.mustAddress[0]
	assert.equal(item.source, 'outside_diff')
	assert.equal(item.cannotReplyInline, true)
	assert.equal(item.decideTarget, 'cr-comment:v1:9c172a7d1afd80bd5b22f105')
	assert.match(item.title, /refresh-family retention/i)
	assert.ok(r.exampleInvokes.some((i) => i.command.includes('cr-comment:v1:9c172a7d1afd80bd5b22f105')))
	assert.ok(!r.exampleInvokes.some((i) => i.command.includes('ship-pr/reply-review')))

	const decided = computeShip(
		snap({ ...base, reviews }),
		[decision('thread', 'cr-comment:v1:9c172a7d1afd80bd5b22f105', 'accepted')],
		[],
		opts(),
	)
	assert.equal(decided.focus, 'merge')
})

// kentcdodds/kody#3141: tick marked the draft ready, re-read before the
// ready-for-review runs existed, and reported "3 passing, 9 skipped" as done.
const READY_AT = '2026-10-05T19:59:40Z'
const draftEra = { startedAt: '2026-10-05T19:58:00Z', completedAt: '2026-10-05T19:58:10Z' }
const draftEraChecks = [
	check('โœ๏ธ CLA', 'COMPLETED', 'SUCCESS', draftEra),
	check('๐Ÿšง Merge conflicts', 'COMPLETED', 'SUCCESS', draftEra),
	check('Report merge conflicts', 'COMPLETED', 'SUCCESS', draftEra),
	...['โœ… Validate', '๐Ÿงน Static', '๐Ÿงช Node', 'โ˜๏ธ Workers', '๐Ÿ”Œ MCP', '๐ŸŽญ E2E', '๐Ÿ”Ž Deploy Preview Resources', '๐Ÿงน Cleanup Preview Resources', '๐Ÿ“ Record CLA'].map(
		(name) => check(name, 'COMPLETED', 'SKIPPED', draftEra),
	),
	bugbotDone,
]
const run = (id: number, name: string, status: string, conclusion: string | null, createdAt: string) => ({
	id,
	name,
	event: 'pull_request',
	status,
	conclusion,
	url: `https://github.com/kentcdodds/kody/actions/runs/${id}`,
	createdAt,
})
const draftRuns = [run(1, 'โœ… Validate', 'completed', 'skipped', '2026-10-05T19:57:59Z'), run(2, 'โœ๏ธ CLA', 'completed', 'success', '2026-10-05T19:57:59Z')]
const ciStep = (r: ReturnType<typeof computeShip>) => [...r.done, ...r.remaining].find((x) => x.id === 'ci')!

test('ci waits right after ready-for-review even when draft-era checks are all terminal', () => {
	const r = computeShip(snap({ readyForReviewAt: READY_AT, checks: draftEraChecks, headRuns: draftRuns }), [], [], opts())
	assert.equal(ciStep(r).status, 'waiting')
	assert.match(ciStep(r).summary, /marked ready 20s ago/)
	assert.notEqual(r.focus, 'merge')
	assert.equal(r.focus, 'ci')
	assert.equal(r.exit.status, 'waiting')
})

test('ci waits on queued workflow runs that have not reported check runs yet', () => {
	const headRuns = [...draftRuns, run(3, 'โœ… Validate', 'queued', null, '2026-10-05T19:59:42Z'), run(4, '๐Ÿ”Ž Preview', 'in_progress', null, '2026-10-05T19:59:42Z')]
	const r = computeShip(snap({ readyForReviewAt: READY_AT, checks: draftEraChecks, headRuns }), [], [], opts())
	const ci = ciStep(r)
	assert.equal(ci.status, 'waiting')
	assert.match(ci.summary, /2 workflow runs unfinished/)
	assert.deepEqual((ci.details!.pendingRuns as Array<{ name: string }>).map((x) => x.name), ['โœ… Validate', '๐Ÿ”Ž Preview'])
	// still waiting long after ready while runs are unfinished
	const later = computeShip(snap({ readyForReviewAt: READY_AT, checks: draftEraChecks, headRuns }), [], [], opts({ now: NOW + 3_600_000 }))
	assert.equal(ciStep(later).status, 'waiting')
})

test('ci waits while ready-for-review jobs are pending, done once every head-SHA run is terminal', () => {
	const fresh = { startedAt: '2026-10-05T19:59:45Z' }
	const pendingChecks = [
		...draftEraChecks,
		...['โœ… Validate', '๐Ÿงน Static', '๐Ÿงช Node'].map((name) => check(name, 'IN_PROGRESS', null, fresh)),
	]
	const inFlight = [...draftRuns, run(3, 'โœ… Validate', 'in_progress', null, '2026-10-05T19:59:42Z')]
	const waiting = computeShip(snap({ readyForReviewAt: READY_AT, checks: dedupeChecks(pendingChecks).checks, headRuns: inFlight }), [], [], opts())
	assert.equal(ciStep(waiting).status, 'waiting')
	assert.match(ciStep(waiting).summary, /^3 pending/)

	const doneChecks = [
		...draftEraChecks,
		...['โœ… Validate', '๐Ÿงน Static', '๐Ÿงช Node'].map((name) => check(name, 'COMPLETED', 'SUCCESS', { startedAt: '2026-10-05T19:59:45Z', completedAt: '2026-10-05T19:59:55Z' })),
	]
	const finished = [...draftRuns, run(3, 'โœ… Validate', 'completed', 'success', '2026-10-05T19:59:42Z')]
	const done = computeShip(snap({ readyForReviewAt: READY_AT, checks: dedupeChecks(doneChecks).checks, headRuns: finished }), [], [], opts())
	assert.equal(ciStep(done).status, 'done')
	assert.equal(done.focus, 'merge')
})

test('ci needs attention when draft-skipped jobs never re-ran after ready', () => {
	const r = computeShip(snap({ readyForReviewAt: READY_AT, checks: draftEraChecks, headRuns: draftRuns }), [], [], opts({ now: NOW + 600_000 }))
	const ci = ciStep(r)
	assert.equal(ci.status, 'needs_attention')
	assert.match(ci.summary, /9 skipped while draft/)
	assert.ok((ci.details!.skippedWhileDraft as Array<string>).includes('โœ… Validate'))
})

test('ci waits for CI to start on a fresh push or a just-opened PR', () => {
	const pushed = computeShip(snap({ headCommittedDate: '2026-10-05T19:59:00Z', createdAt: '2026-10-05T18:00:00Z' }), [], [], opts())
	assert.equal(ciStep(pushed).status, 'waiting')
	assert.match(ciStep(pushed).summary, /no CI reported yet/)
	const opened = computeShip(snap({ createdAt: '2026-10-05T19:59:30Z', checks: [check('push-ci', 'COMPLETED', 'SUCCESS')] }), [], [], opts())
	assert.equal(ciStep(opened).status, 'waiting')
	assert.match(ciStep(opened).summary, /PR opened 30s ago/)
	const settledPush = computeShip(snap({ checks: [check('๐Ÿงช Node', 'COMPLETED', 'SUCCESS'), bugbotDone] }), [], [], opts())
	assert.equal(ciStep(settledPush).status, 'done')
})

test('deploy: concurrency-cancelled runs on the merge SHA are superseded once production contains the merge', () => {
	const mergeSha = 'c'.repeat(40)
	const served = 'd'.repeat(40)
	const cancelledRuns = [
		{ id: 38081032095, name: 'โœ… Validate', event: 'push', status: 'completed', conclusion: 'cancelled', url: 'https://github.com/kentcdodds/kody/actions/runs/38081032095', createdAt: 'x' },
		{ id: 2, name: '๐Ÿš€ Deploy (production)', event: 'workflow_run', status: 'completed', conclusion: 'skipped', url: 'u', createdAt: 'x' },
	]
	const merged = { state: 'MERGED' as const, merged: true, mergedAt: '2026-10-05T19:55:00Z', mergeCommitSha: mergeSha, deployRuns: cancelledRuns }
	const prod = (over: Record<string, unknown>) => ({ url: 'https://kody.codes/health', commitSha: served, containsMerge: true, compareStatus: 'ahead', error: null, ...over })

	const shipped = analyzeDeploy(snap({ ...merged, production: prod({}) }), NOW)
	assert.equal(shipped.status, 'done')
	assert.match(shipped.summary, /descendant of the merge SHA/)
	assert.match(shipped.summary, /superseded: 1 cancelled/)
	assert.equal(shipped.failed.length, 0)

	const notYet = analyzeDeploy(snap({ ...merged, production: prod({ containsMerge: false, compareStatus: 'behind' }) }), NOW)
	assert.equal(notYet.status, 'waiting')
	assert.match(notYet.summary, /superseded by a newer push/)

	const stale = analyzeDeploy(snap({ ...merged, production: prod({ containsMerge: false, compareStatus: 'behind' }) }), NOW + 60 * 60 * 1000)
	assert.equal(stale.status, 'needs_attention')

	const noHealth = analyzeDeploy(snap({ ...merged, locator: makeLocator('epicweb-dev', 'gratitext', 101), production: null }), NOW)
	assert.equal(noHealth.status, 'needs_attention')
	assert.doesNotMatch(noHealth.summary, /failed/)
	assert.match(noHealth.summary, /Verify a later deploy includes the merge SHA/)

	const realFailure = analyzeDeploy(
		snap({ ...merged, deployRuns: [{ ...cancelledRuns[0], conclusion: 'failure' }], production: prod({}) }),
		NOW,
	)
	assert.equal(realFailure.status, 'needs_attention')
	assert.match(realFailure.summary, /failed on the merge SHA/)
})

test('deploy: production serving the exact merge SHA is done even while runs report late', () => {
	const mergeSha = 'e'.repeat(40)
	const r = analyzeDeploy(
		snap({
			state: 'MERGED',
			merged: true,
			mergedAt: '2026-10-05T19:55:00Z',
			mergeCommitSha: mergeSha,
			deployRuns: [{ id: 1, name: 'โœ… Validate', event: 'push', status: 'completed', conclusion: 'success', url: 'u', createdAt: 'x' }],
			production: { url: 'https://kody.codes/health', commitSha: mergeSha, containsMerge: true, compareStatus: 'identical', error: null },
		}),
		NOW,
	)
	assert.equal(r.status, 'done')
	assert.match(r.summary, /the merge SHA$/)
})