โ Public packages
@kentcdodds/ship-pr
Progressive ship-pr loop for agents: PR status, Bugbot, review sort, 7-day decisions, merge, deploy, wrap-up.
src/status.test.ts
546 lines ยท 28.1 KB ยท TypeScriptimport 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$/)
})