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 { 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 = {}) { 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 { 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).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 = [{ 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 = [ { 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).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).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)** > >
> ๐ŸŸก Minor ยท Update the refresh-family retention description. ยท authentication.md:791-793
> > \`docs/contributing/architecture/authentication.md:791-793\` > **Update the refresh-family retention description.** > > This paragraph says account deletion does not sweep refresh-family keys. > > > >
--- ` 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) => [...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).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) => ({ 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$/) })