// The wipe-day cutover (docs/runicnpc/PLAN.md D332): when it works, what each gate accepts, which issues the // PR closes, what the hourly run does next, and the commands against a fake Gitea and a fake panel. // node --test tools/drill/cutover.test.js const test = require('node:test') const assert = require('node:assert') const fs = require('fs') const os = require('os') const path = require('path') const c = require('./cutover.js') const { wipeWeekend, publicChanged, publicGate, oxideReady, carbonOk, carbonBuild, acfDepotManifest, liveOk, checkState, collectCloses, cutoverBody, withBlock, decide, commentText, neutralise, isDry, pass, fail, persistable, nextGate, manifestTime, GATES, COOL_DOWN_HOURS } = c const at = (s) => new Date(s) const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'cutover-test-')) // ---- wipeWeekend: November 2026 has its wipe on Thursday the 5th, so the weekend ends Sunday the 8th. test('the weekend opens at 17:00 UTC on the wipe Thursday and not a minute before', () => { assert.equal(wipeWeekend(at('2026-11-05T16:59:00Z')).open, false) assert.equal(wipeWeekend(at('2026-11-05T17:00:00Z')).open, true) assert.equal(wipeWeekend(at('2026-11-05T17:00:00Z')).wipe.toISOString(), '2026-11-05T00:00:00.000Z') }) test('it closes at Sunday 23:00 UTC, with one more hour to give up in', () => { assert.equal(wipeWeekend(at('2026-11-08T22:59:00Z')).open, true) const late = wipeWeekend(at('2026-11-08T23:00:00Z')) assert.equal(late.open, false) assert.equal(late.final, true) assert.equal(late.deadline.toISOString(), '2026-11-08T23:00:00.000Z') assert.equal(wipeWeekend(at('2026-11-08T23:59:00Z')).final, true) assert.equal(wipeWeekend(at('2026-11-09T00:00:00Z')).final, false) assert.equal(wipeWeekend(at('2026-11-05T17:00:00Z')).final, false) }) test('a Thursday that is not the first of the month is closed, as is the morning of the wipe', () => { assert.equal(wipeWeekend(at('2026-11-12T18:00:00Z')).open, false) assert.equal(wipeWeekend(at('2026-11-05T09:00:00Z')).open, false) assert.equal(wipeWeekend(at('2026-11-06T12:00:00Z')).open, true) }) test('a month whose first Thursday is the 7th ends its weekend on the 10th', () => { const w = wipeWeekend(at('2026-05-09T12:00:00Z')) assert.equal(w.wipe.toISOString(), '2026-05-07T00:00:00.000Z') assert.equal(w.deadline.toISOString(), '2026-05-10T23:00:00.000Z') assert.equal(w.open, true) assert.equal(wipeWeekend(at('2026-05-10T23:00:00Z')).open, false) }) test('when the 1st is the Thursday, the weekend is the 1st to the 4th', () => { const w = wipeWeekend(at('2026-01-02T00:00:00Z')) assert.equal(w.wipe.toISOString(), '2026-01-01T00:00:00.000Z') assert.equal(w.deadline.toISOString(), '2026-01-04T23:00:00.000Z') assert.equal(w.open, true) assert.equal(wipeWeekend(at('2026-01-01T16:59:00Z')).open, false) }) // ---- the manifest date, strictly test('manifestTime reads M/D/YYYY HH:mm:ss in 24 hours, with AM/PM and ISO fallbacks, and nothing else', () => { assert.equal(manifestTime('10/31/2026 11:48:58'), Date.UTC(2026, 9, 31, 11, 48, 58)) assert.equal(manifestTime('11/5/2026 13:00:00'), Date.UTC(2026, 10, 5, 13, 0, 0)) assert.equal(manifestTime('11/5/2026 12:00:00 AM'), Date.UTC(2026, 10, 5, 0, 0, 0)) assert.equal(manifestTime('11/5/2026 01:30:00 PM'), Date.UTC(2026, 10, 5, 13, 30, 0)) assert.equal(manifestTime('2026-11-05T09:00:00Z'), Date.UTC(2026, 10, 5, 9, 0, 0)) for (const bad of ['', undefined, '?', 'yesterday', '13/40/2026 01:00:00', '11/31/2026 01:00:00', '11/5/2026 25:00:00', '11/5/2026 13:00:00 PM', '5.11.2026 10:00:00', '11/5/26 10:00:00']) { assert.ok(Number.isNaN(manifestTime(bad)), `${bad} should not parse`) } }) // ---- the gates test('publicChanged: the manifest must be dated the wipe day or later (UTC)', () => { const wipe = at('2026-11-05T00:00:00Z') assert.equal(publicChanged({ manifestDate: '11/4/2026 23:59:59' }, wipe), false) assert.equal(publicChanged({ manifestDate: '11/5/2026 00:00:00' }, wipe), true) assert.equal(publicChanged({ manifestDate: '10/31/2026 11:48:58' }, wipe), false) assert.equal(publicChanged({ manifestDate: 'yesterday' }, wipe), false) assert.equal(publicChanged(undefined, wipe), false) }) test('publicGate: public must also be the build staging has now (the promotion went through)', () => { const wipe = at('2026-11-05T00:00:00Z') const pub = { manifest: '8140', manifestDate: '11/5/2026 11:48:58' } assert.equal(publicGate(pub, { manifest: '8140' }, wipe), true) assert.match(publicGate(pub, { manifest: '8150' }, wipe), /not its current staging build \(manifest 8150\)/) assert.match(publicGate(pub, { note: 'no Assembly' }, wipe), /staging build could not be read/) assert.match(publicGate(pub, null, wipe), /staging build could not be read/) assert.match(publicGate({ manifest: '8100', manifestDate: '10/31/2026 11:48:58' }, { manifest: '8100' }, wipe), /from before the wipe/) assert.match(publicGate({ manifest: '8140', manifestDate: 'garbage', note: 'n' }, { manifest: '8140' }, wipe), /no usable manifest date/) }) test('oxideReady: true, or the reason', () => { const ok = { managed: 'ok', oxideBehind: 0, compile: 'ok', fields: 'same' } assert.equal(oxideReady(ok), true) assert.match(oxideReady({ ...ok, oxideBehind: 12 }), /Oxide's public build is 12 member\(s\) behind/) assert.match(oxideReady({ ...ok, fields: 'differs' }), /field list differs from public Rust's/) assert.match(oxideReady({ ...ok, compile: 'errors' }), /does not compile/) assert.match(oxideReady({ ...ok, managed: 'error', note: 'DepotDownloader could not be fetched' }), /could not fetch Rust's public build \(DepotDownloader/) assert.match(oxideReady({ ...ok, oxideBehind: -1 }), /could not be compared/) assert.match(oxideReady({ ...ok, fields: 'error' }), /could not be checked/) assert.match(oxideReady(undefined), /could not fetch/) }) const ACF = `"AppState" { \t"appid"\t\t"258550" \t"buildid"\t\t"22222222" \t"InstalledDepots" \t{ \t\t"258551" \t\t{ \t\t\t"manifest"\t\t"111" \t\t\t"size"\t\t"1" \t\t} \t\t"258552" \t\t{ \t\t\t"manifest"\t\t"8140123456789" \t\t\t"size"\t\t"9000000000" \t\t} \t} \t"SharedDepots" \t{ \t\t"228984"\t\t"228990" \t} } ` test('the appmanifest: the Linux depot\'s manifest, and null when it is not there', () => { assert.equal(acfDepotManifest(ACF), '8140123456789') assert.equal(acfDepotManifest(ACF, '258551'), '111') assert.equal(acfDepotManifest(ACF, '999'), null) assert.equal(acfDepotManifest(''), null) assert.equal(acfDepotManifest(undefined), null) assert.equal(acfDepotManifest('"AppState" { "InstalledDepots" { "258552" "oops" } }'), null) }) test('carbonBuild: the rig must run the public manifest', () => { assert.equal(carbonBuild(ACF, '8140123456789'), true) assert.match(carbonBuild(ACF, '9999'), /rust-carbon still runs manifest 8140123456789 \(public is 9999\)/) assert.match(carbonBuild(null, '9999'), /could not be read/) }) const STATUS = 'framework=carbon navmesh=ready swap fields: npc=58/58 brain=32/32 missing=- added=-' test('carbonOk: a Carbon server with the navmesh ready and nothing missing or added', () => { assert.equal(carbonOk(['RunicNPC 0.0.0 api=1 hooks=40 fired=3 silent=0', 'fired: x', STATUS, 'npcs=0']), true) assert.equal(carbonOk(STATUS), true) }) test('carbonOk: each way it can fail says why', () => { assert.match(carbonOk([STATUS.replace('added=-', 'added=BasePlayer.x')]), /does not know: BasePlayer\.x/) assert.match(carbonOk([STATUS.replace('missing=-', 'missing=BaseNpc.y,Brain.z')]), /missing on Carbon: BaseNpc\.y,Brain\.z/) assert.match(carbonOk([STATUS.replace('framework=carbon', 'framework=oxide')]), /framework=oxide, not carbon/) assert.match(carbonOk([STATUS.replace('navmesh=ready', 'navmesh=building')]), /navmesh=building/) assert.match(carbonOk([]), /never answered/) assert.match(carbonOk(['RunicNPC 0.0.0 api=1']), /never answered/) }) test('liveOk: ran, harness done, some checks passed, no failed check and no finding', () => { const good = { branch: 'public', ran: true, findings: [], harness: { done: true, pass: 266, fail: 0, failed: [] } } assert.equal(liveOk(good), true) assert.match(liveOk(null), /no live\.json/) assert.match(liveOk({ ...good, ran: false, findings: ['The server did not start on Rust public 1'] }), /did not start/) assert.match(liveOk({ ...good, ran: false, skipped: "Oxide's public build is 3 member(s) behind" }), /3 member/) assert.match(liveOk({ ...good, harness: { done: false, pass: 5, fail: 0, failed: [] }, findings: ['The harness stopped after x'] }), /harness stopped after x/) assert.match(liveOk({ ...good, harness: { done: true, pass: 5, fail: 1, failed: ['place.set: FAIL off the navmesh'] } }), /place\.set: FAIL off the navmesh/) assert.match(liveOk({ ...good, findings: ['RunicNPC logged 2 error line(s) in Oxide\'s log during the run.'] }), /logged 2 error/) assert.match(liveOk({ ...good, branch: 'staging' }), /staging branch/) assert.match(liveOk({ ...good, harness: { done: true, pass: 0, fail: 0, failed: [] } }), /passed no check/) }) test('checkState: success needs a plugin-checks context; a failure or a wait is said', () => { const s = (context, status) => ({ context, status }) assert.equal(checkState({ state: 'success', statuses: [s('PR Checks / plugin-checks (pull_request)', 'success')] }).state, 'success') assert.equal(checkState({ state: 'success', statuses: [] }).state, 'pending') assert.equal(checkState({ state: 'success', statuses: [s('other', 'success')] }).state, 'pending') assert.equal(checkState({ state: 'pending', statuses: [s('PR Checks / plugin-checks (pull_request)', 'pending')] }).state, 'pending') const failed = checkState({ state: 'failure', statuses: [s('PR Checks / plugin-checks (pull_request)', 'failure')] }) assert.equal(failed.state, 'failed') assert.match(failed.reason, /plugin-checks.*failure/) }) // ---- what the PR closes, and what is written into it test('collectCloses reads closing keywords before #N, in any case', () => { assert.deepEqual(collectCloses(['Closes #20', 'fixes #3', 'RESOLVED #9', 'Fixed #12.', 'close #4', 'Resolves: #8']), [3, 4, 8, 9, 12, 20]) }) test('collectCloses takes several on a line, and dedupes and sorts', () => { assert.deepEqual(collectCloses(['Closes #22, closes #20 and fixes #5', 'Closes #20\nCloses #5', 'closes #100']), [5, 20, 22, 100]) }) test('collectCloses ignores references, bare numbers, prose and other repositories', () => { assert.deepEqual(collectCloses(['Refs #21', 'see #21', '#21', 'This closes the loop on #21', 'prefix #3', 'unfixes #4', 'Closes RunicGateway/docs#9', '']), []) assert.deepEqual(collectCloses([undefined, null]), []) }) test('cutoverBody names the wipe and the build, lists a Closes line per issue, and ends with the disclosure', () => { const body = cutoverBody({ wipe: '2026-11-05', manifest: '8140', buildDate: '11/5/2026 11:48:58', closes: [20, 22], gates: [ { name: '1. Rust public', status: 'passed', note: '2026-11-05 17:11 UTC' }, { name: '2. Oxide', status: 'failed', note: 'a | b' }, ], }) assert.match(body, /D332/) assert.match(body, /manifest \*\*8140\*\*, dated 11\/5\/2026 11:48:58/) assert.match(body, /^Closes #20$/m) assert.match(body, /^Closes #22$/m) assert.match(body, /\| 2\. Oxide \| failed \| a \/ b \|/) assert.ok(body.endsWith('Opened by the runicnpc-rust cutover workflow (D332).')) assert.match(cutoverBody({ wipe: 'w', manifest: 'm', closes: [], gates: [] }), /No issue is closed/) }) test('a reason can never close an issue: #123 is broken up in the body and in comments', () => { assert.equal(neutralise('fixes #123 and #4'), 'fixes #​123 and #​4') const body = cutoverBody({ wipe: 'w', manifest: 'm', closes: [], gates: [{ name: 'g', status: 'failed', note: 'Closes #5: fixes #123' }] }) assert.deepEqual(collectCloses([body]), []) const text = commentText({ action: 'blocked', gate: 'oxide', reason: 'resolves #7' }) assert.deepEqual(collectCloses([text]), []) // The deliberate lines still close. assert.deepEqual(collectCloses([cutoverBody({ wipe: 'w', manifest: 'm', closes: [9], gates: [] })]), [9]) }) test('withBlock appends the marked block, replaces only what is between the markers, and is stable', () => { const once = withBlock('A human wrote this.', 'one') assert.equal(once, 'A human wrote this.\n\n\none\n') const twice = withBlock(once + '\n\nAnd this after.', 'two') assert.equal(twice, 'A human wrote this.\n\n\ntwo\n\n\nAnd this after.') assert.equal(withBlock(twice, 'two'), twice) assert.equal(withBlock('', 'x'), '\nx\n') }) // ---- decide const THU = at('2026-11-05T18:11:00Z') const obs = (more = {}) => ({ manifest: '8140', edgeSha: 'sha1', ahead: 3, openPr: null, ...more }) const fresh = () => decide(undefined, obs(), THU) const passed = (state, ...names) => names.reduce((s, g) => pass(s, g, THU), state) test('a first run starts at the public gate and asks to be written', () => { const d = fresh() assert.equal(d.action, 'run') assert.equal(d.gate, 'public') assert.equal(d.write, true) assert.equal(d.state.wipe, '2026-11-05') assert.equal(d.state.manifest, '8140') assert.equal(d.state.edgeSha, 'sha1') }) test('the gates run in order, checks comes last and is never kept, and then it merges', () => { assert.deepEqual(GATES, ['public', 'oxide', 'carbon', 'live', 'checks']) let s = fresh().state for (const g of GATES.slice(0, 4)) { const d = decide(s, obs(), THU) assert.equal(d.gate, g) s = pass(d.state, g, THU) } const checks = decide(s, obs(), THU) assert.equal(checks.action, 'run') assert.equal(checks.gate, 'checks') const done = decide(s, obs({ checksPassed: true }), THU) assert.equal(done.action, 'merge') assert.equal(done.merge, true) assert.equal(nextGate({ public: {}, carbon: {} }), 'oxide') // A checks entry found in a stored state is dropped. const old = decide({ ...s, gates: { ...s.gates, checks: { at: 'x' } } }, obs(), THU) assert.equal(old.state.gates.checks, undefined) assert.equal(old.gate, 'checks') assert.equal(persistable({ ...s, gates: { ...s.gates, checks: {} } }, { number: 4 }).gates.checks, undefined) assert.equal(persistable(s, { number: 4 }).pr, 4) }) test('a passed gate is never run again for the same build and the same code', () => { const s = passed(fresh().state, 'public', 'oxide', 'carbon') const d = decide(s, obs(), at('2026-11-05T19:11:00Z')) assert.equal(d.action, 'run') assert.equal(d.gate, 'live') assert.ok(d.state.gates.carbon) }) test('a new public manifest starts the gates again, keeping the open PR', () => { const s = { ...passed(fresh().state, 'public', 'oxide', 'carbon'), pr: 9 } const d = decide(s, obs({ manifest: '8141', openPr: 9 }), at('2026-11-06T10:11:00Z')) assert.equal(d.gate, 'public') assert.deepEqual(d.state.gates, {}) assert.equal(d.state.manifest, '8141') assert.equal(d.state.pr, 9) assert.equal(decide(s, obs({ manifest: '8141', openPr: null }), at('2026-11-06T10:11:00Z')).state.pr, null) }) test('a new edge commit clears every gate and every cooled failure, so nothing proven on old code counts', () => { const s = fail(passed(fresh().state, 'public', 'oxide'), 'carbon', 'x', THU) const d = decide({ ...s, mergeError: 'old' }, obs({ edgeSha: 'sha2' }), at('2026-11-05T19:11:00Z')) assert.equal(d.action, 'run') assert.equal(d.gate, 'public') assert.deepEqual(d.state.gates, {}) assert.deepEqual(d.state.failed, {}) assert.equal(d.state.edgeSha, 'sha2') assert.equal(d.state.mergeError, undefined) // A state written before edgeSha was kept counts as changed code. const legacy = { wipe: '2026-11-05', manifest: '8140', gates: { public: { at: 'x' } } } assert.equal(decide(legacy, obs(), THU).gate, 'public') }) test('a new wipe forgets everything, comments and a give-up included', () => { const s = { ...passed(fresh().state, 'public'), lastComment: '2026-11-05', gaveUp: true } const d = decide(s, obs(), at('2026-12-03T18:00:00Z')) assert.equal(d.action, 'run') assert.equal(d.state.wipe, '2026-12-03') assert.deepEqual(d.state.gates, {}) assert.equal(d.state.lastComment, undefined) assert.equal(d.state.gaveUp, undefined) }) test('nothing to do when edge has no commit main lacks, or when it already merged', () => { const d = decide(undefined, obs({ ahead: 0 }), THU) assert.equal(d.action, 'nothing') assert.match(d.reason, /no commits/) assert.equal(decide({ ...fresh().state, merged: true }, obs(), THU).action, 'nothing') }) test('the cool-down: a failed carbon or live is not tried again for three hours, a new commit retries at once', () => { assert.equal(COOL_DOWN_HOURS, 3) const base = passed(fresh().state, 'public', 'oxide') const failedCarbon = fail(base, 'carbon', 'did not come up', THU) // failed at 18:11 const at59 = decide(failedCarbon, obs(), at('2026-11-05T21:10:00Z')) // 2 h 59 min later assert.equal(at59.action, 'cooling') assert.equal(at59.gate, 'carbon') assert.equal(at59.retryAt.toISOString(), '2026-11-05T21:11:00.000Z') assert.match(at59.reason, /did not come up/) assert.equal(decide(failedCarbon, obs(), at('2026-11-05T21:11:00Z')).action, 'run') // 3 h 00 assert.equal(decide(failedCarbon, obs(), at('2026-11-05T21:11:00Z')).gate, 'carbon') assert.equal(decide(failedCarbon, obs({ edgeSha: 'sha2' }), at('2026-11-05T19:00:00Z')).gate, 'public') assert.equal(decide(failedCarbon, obs({ manifest: '8141' }), at('2026-11-05T19:00:00Z')).gate, 'public') // The same rule for live; public and oxide are cheap and never cool. const failedLive = fail(passed(base, 'carbon'), 'live', 'a check failed', THU) assert.equal(decide(failedLive, obs(), at('2026-11-05T20:00:00Z')).action, 'cooling') assert.equal(decide(failedLive, obs(), at('2026-11-05T21:11:00Z')).gate, 'live') assert.equal(decide(fail(fresh().state, 'oxide', 'x', THU), obs(), at('2026-11-05T18:30:00Z')).gate, 'public') // A pass forgets the failure. assert.equal(pass(failedCarbon, 'carbon', THU).failed.carbon, undefined) }) test('a cooling gate comments once a day and says when it will try again', () => { const s = fail(passed(fresh().state, 'public', 'oxide'), 'carbon', 'did not come up', THU) const d = decide(s, obs({ openPr: 9 }), at('2026-11-05T19:11:00Z')) assert.equal(d.comment, true) assert.match(commentText(d, d.state), /not try this gate again before 2026-11-05 21:11 UTC/) assert.match(commentText(d, d.state), /3\. A Carbon boot/) assert.equal(decide(d.state, obs({ openPr: 9 }), at('2026-11-05T20:11:00Z')).comment, false) }) test('a blocked gate comments once a day, and only after the public gate passed', () => { const blocked = { gate: 'oxide', reason: "Oxide's public build is 12 member(s) behind" } const s = passed(fresh().state, 'public') const first = decide(s, obs({ openPr: 9, blocked }), THU) assert.equal(first.action, 'blocked') assert.equal(first.comment, true) assert.equal(first.state.lastComment, '2026-11-05') assert.equal(decide(first.state, obs({ openPr: 9, blocked }), at('2026-11-05T19:11:00Z')).comment, false) assert.equal(decide(first.state, obs({ openPr: 9, blocked }), at('2026-11-06T09:11:00Z')).comment, true) assert.equal(decide(fresh().state, obs({ openPr: 9, blocked: { gate: 'public', reason: 'old' } }), THU).comment, false) assert.equal(decide(s, obs({ openPr: null, blocked }), THU).comment, false) // A blocked carbon says when it will be tried again. const carbon = decide(s, obs({ openPr: 9, blocked: { gate: 'carbon', reason: 'x' } }), THU) assert.equal(carbon.retryAt.toISOString(), '2026-11-05T21:11:00.000Z') }) test('after the deadline it gives up once, with a last comment', () => { const s = passed(fresh().state, 'public', 'oxide') const late = at('2026-11-08T23:11:00Z') const d = decide(s, obs({ openPr: 9 }), late) assert.equal(d.action, 'giveUp') assert.equal(d.gate, 'carbon') assert.equal(d.comment, true) assert.equal(d.state.gaveUp, true) assert.match(commentText(d, d.state), /Gate "3\. A Carbon boot on public Rust" had not passed/) const again = decide(d.state, obs({ openPr: 9 }), at('2026-11-08T23:40:00Z')) assert.equal(again.action, 'nothing') assert.equal(again.comment, false) assert.equal(decide(s, obs({ openPr: null }), late).comment, false) assert.equal(decide(s, obs({ openPr: 9, forced: true }), late).action, 'run') }) test('giving up with every gate passed and the merge failed says so, never gate "undefined"', () => { const s = { ...passed(fresh().state, 'public', 'oxide', 'carbon', 'live'), mergeError: 'POST /pulls/5/merge: 405 not mergeable' } const d = decide(s, obs({ openPr: 5 }), at('2026-11-08T23:11:00Z')) assert.equal(d.action, 'giveUp') assert.equal(d.gate, undefined) const text = commentText(d, d.state) assert.match(text, /Every gate passed; the merge failed: POST \/pulls\/5\/merge: 405 not mergeable/) assert.ok(!text.includes('undefined')) assert.ok(!commentText(d, { ...d.state, mergeError: undefined }).includes('undefined')) }) test('a dry run never asks to write, comment or merge', () => { const dry = { dryRun: true } assert.equal(decide(undefined, obs(), THU, dry).write, false) const s = passed(fresh().state, 'public') const b = decide(s, obs({ openPr: 9, blocked: { gate: 'oxide', reason: 'x' } }), THU, dry) assert.equal(b.comment, false) assert.equal(b.write, false) assert.equal(b.state.lastComment, undefined) const m = decide(passed(fresh().state, ...GATES.slice(0, 4)), obs({ checksPassed: true }), THU, dry) assert.equal(m.action, 'merge') assert.equal(m.merge, false) const g = decide(s, obs({ openPr: 9 }), at('2026-11-08T23:11:00Z'), dry) assert.equal(g.action, 'giveUp') assert.equal(g.comment, false) assert.equal(decide(undefined, obs({ ahead: 0 }), THU, dry).action, 'run') }) test('a dispatch is dry unless its dry_run input is exactly false', () => { const dispatch = (input) => ({ GITHUB_EVENT_NAME: 'workflow_dispatch', DRY_RUN_INPUT: input }) assert.equal(isDry({}, dispatch(undefined)), true) assert.equal(isDry({}, dispatch('')), true) assert.equal(isDry({}, dispatch('true')), true) assert.equal(isDry({}, dispatch('maybe')), true) assert.equal(isDry({}, dispatch('false')), false) assert.equal(isDry({}, { GITHUB_EVENT_NAME: 'schedule' }), false) assert.equal(isDry({ 'dry-run': true }, { GITHUB_EVENT_NAME: 'schedule' }), true) }) // ---- the commands, against a fake Gitea and a fake panel const GIT = (...a) => ({ 'rev-parse': 'sha1', 'rev-list': '3', 'merge-base': 'mb', log: '2026-11-01T00:00:00+00:00' }[a[0]] || '') /** deps with a fake fetch: handler(method, path, body) -> { status, json }; every call is recorded. */ function fakeDeps({ now = '2026-11-05T18:11:00Z', handler, git = GIT, panel, env = {} }) { const deps = { calls: [], at: now, env: { GITHUB_SERVER_URL: 'http://gitea.test', GITHUB_REPOSITORY: 'o/r', GITEA_TOKEN: 'reg-token', MERGE_TOKEN: 'merge-token', ...env }, now: () => new Date(deps.at), sleep: async () => {}, git: (...a) => git(...a), panel, fetch: async (url, o) => { const p = url.replace('http://gitea.test/api/v1/repos/o/r', '') const body = o.body ? JSON.parse(o.body) : undefined deps.calls.push({ method: o.method, p, body, auth: o.headers.Authorization }) const r = handler(o.method, p, body) || {} const status = r.status || 200 return { ok: status < 400, status, text: async () => (r.json === undefined ? '' : JSON.stringify(r.json)) } }, } return deps } const STAT = { branch: 'public', manifest: '8140', manifestDate: '11/5/2026 11:48:58', managed: 'ok', oxideBehind: 0, fields: 'same', compile: 'ok' } function rig(stat = STAT, staging = { branch: 'staging', manifest: '8140' }) { const dir = tmp() fs.mkdirSync(path.join(dir, 'work')) fs.mkdirSync(path.join(dir, 'work-staging')) fs.writeFileSync(path.join(dir, 'work', 'static.json'), JSON.stringify(stat)) fs.writeFileSync(path.join(dir, 'work-staging', 'static.json'), JSON.stringify(staging)) const state = path.join(dir, 'drill.json') fs.writeFileSync(state, '{}') const a = { state, work: path.join(dir, 'work'), 'staging-work': path.join(dir, 'work-staging') } return { dir, a, state, read: () => JSON.parse(fs.readFileSync(state, 'utf8')) } } /** A Gitea with one edge to main PR that exists once POSTed. */ function fakeGitea() { const g = { pr: null, comments: [] } g.handler = (method, p, body) => { if (method === 'GET' && p.startsWith('/pulls?state=open')) return { json: g.pr ? [g.pr] : [] } if (method === 'GET' && p.startsWith('/pulls?state=closed')) return { json: [] } if (method === 'POST' && p === '/pulls') { if (g.failCreate) return { status: 500, json: { message: 'boom' } } g.pr = { number: 5, head: { ref: 'edge', sha: 'sha1' }, base: { ref: 'main' }, ...body, head_: undefined } g.pr.head = { ref: 'edge', sha: 'sha1' } g.pr.base = { ref: 'main' } return { json: g.pr } } if (method === 'PATCH' && p === '/pulls/5') { g.pr = { ...g.pr, ...body } return { json: g.pr } } if (method === 'POST' && p === '/issues/5/comments') { g.comments.push(body.body) return { json: {} } } return { json: [] } } return g } test('gates in a dry run creates, edits, comments and writes nothing', async () => { const r = rig() const g = fakeGitea() const deps = fakeDeps({ handler: g.handler, panel: () => ({ servers: async () => { throw new Error('panel down') } }) }) const d = await c.gates({ ...r.a, 'dry-run': true }, deps) assert.ok(deps.calls.length > 0) assert.ok(deps.calls.every((x) => x.method === 'GET'), JSON.stringify(deps.calls.filter((x) => x.method !== 'GET'))) assert.equal(fs.readFileSync(r.state, 'utf8'), '{}') assert.equal(d.comment, false) assert.equal(g.pr, null) assert.equal(g.comments.length, 0) // It ran past the failed carbon gate to try the others. const run = JSON.parse(fs.readFileSync(path.join(r.dir, 'work', 'cutover-run.json'), 'utf8')) assert.equal(run.rows.public.status, 'passed') assert.equal(run.rows.oxide.status, 'passed') assert.equal(run.rows.carbon.status, 'failed') assert.equal(run.rows.checks.status, 'failed') }) test('a workflow_dispatch without dry_run=false is dry even without the flag', async () => { const r = rig() const g = fakeGitea() const deps = fakeDeps({ handler: g.handler, env: { GITHUB_EVENT_NAME: 'workflow_dispatch' }, panel: () => ({ servers: async () => { throw new Error('down') } }) }) await c.gates(r.a, deps) assert.ok(deps.calls.every((x) => x.method === 'GET')) assert.equal(fs.readFileSync(r.state, 'utf8'), '{}') }) test('a real run keeps each gate, opens the PR, comments once, and cools a failed carbon for three hours', async () => { const r = rig() const g = fakeGitea() let panelCalls = 0 const deps = fakeDeps({ handler: g.handler, panel: () => ({ servers: async () => { panelCalls++; throw new Error('panel down') } }) }) await c.gates(r.a, deps) // 18:11 let s = r.read().cutover assert.equal(s.edgeSha, 'sha1') assert.deepEqual(Object.keys(s.gates).sort(), ['oxide', 'public']) assert.match(s.failed.carbon.reason, /panel down/) assert.equal(s.pr, 5) assert.equal(s.lastComment, '2026-11-05') assert.equal(panelCalls, 1) assert.ok(g.pr.title.startsWith('chore(release): RunicNPC cutover for the 2026-11-05 wipe, Rust 8140')) assert.ok(g.pr.body.includes('')) assert.equal(g.comments.length, 1) assert.match(g.comments[0], /not try this gate again before 2026-11-05 21:11 UTC/) assert.ok(deps.calls.filter((x) => x.method !== 'GET').every((x) => x.auth === 'token reg-token')) deps.at = '2026-11-05T19:11:00Z' await c.gates(r.a, deps) assert.equal(panelCalls, 1, 'cooling: the rig is not touched') deps.at = '2026-11-05T21:10:00Z' await c.gates(r.a, deps) assert.equal(panelCalls, 1, '2 h 59 min') assert.equal(g.comments.length, 1, 'one comment a day') deps.at = '2026-11-05T21:11:00Z' await c.gates(r.a, deps) assert.equal(panelCalls, 2, '3 h 00 min: it tries again') // New code: everything again, at once. deps.git = (...a) => (a[0] === 'rev-parse' ? 'sha2' : GIT(...a)) deps.at = '2026-11-05T21:30:00Z' await c.gates(r.a, deps) s = r.read().cutover assert.equal(panelCalls, 3) assert.equal(s.edgeSha, 'sha2') assert.ok(s.gates.public && s.gates.oxide) }) test('a PR call that fails never loses a gate that passed', async () => { const r = rig() const g = fakeGitea() g.failCreate = true const deps = fakeDeps({ handler: g.handler, panel: () => ({ servers: async () => { throw new Error('panel down') } }) }) await c.gates(r.a, deps) const s = r.read().cutover assert.ok(s.gates.public) assert.ok(s.gates.oxide) assert.equal(s.pr, null) }) test('gate 1 blocks, and nothing else runs, when public is not what staging has', async () => { const r = rig(STAT, { branch: 'staging', manifest: '8999' }) const g = fakeGitea() let panelCalls = 0 const deps = fakeDeps({ handler: g.handler, panel: () => ({ servers: async () => { panelCalls++; throw new Error('x') } }) }) await c.gates(r.a, deps) const s = r.read().cutover assert.deepEqual(s.gates, {}) assert.equal(panelCalls, 0) assert.equal(g.pr, null) }) test('checksGate refuses a PR whose head is not the commit the gates ran on', async () => { const deps = fakeDeps({ handler: (m, p) => (p === '/pulls/3' ? { json: { number: 3, head: { sha: 'zzzzzzzz1' } } } : { json: {} }) }) const ctx = { g: c.gitea('t', deps), deps, dryRun: false, edgeSha: 'aaaaaaaa1', pr: { number: 3 } } assert.match(await c.checksGate(ctx), /head zzzzzzzz is not the commit the gates ran on \(aaaaaaaa\)/) assert.ok(!deps.calls.some((x) => x.p.includes('/status'))) }) test('merge sends Do=merge, keeps edge, names the head commit, and verifies it merged', async () => { let merged = false const deps = fakeDeps({ handler: (method, p, body) => { if (method === 'POST' && p === '/pulls/7/merge') { merged = true return {} } if (method === 'GET' && p === '/pulls/7') return { json: { number: 7, head: { sha: 'abc123' }, merged } } return {} }, }) await c.merge({ pr: '7', sha: 'abc123' }, deps) const post = deps.calls.find((x) => x.method === 'POST') assert.deepEqual(post.body, { Do: 'merge', delete_branch_after_merge: false, head_commit_id: 'abc123' }) assert.equal(post.auth, 'token merge-token') assert.equal(deps.calls[deps.calls.length - 1].method, 'GET') }) test('merge refuses a moved head, and a merge that did not happen, and records why', async () => { const r = rig() fs.writeFileSync(r.state, JSON.stringify({ cutover: { wipe: '2026-11-05' } })) const moved = fakeDeps({ handler: (m, p) => (p === '/pulls/7' ? { json: { head: { sha: 'other' }, merged: false } } : {}) }) await assert.rejects(c.merge({ pr: '7', sha: 'abc123', state: r.state }, moved), /not the commit the gates ran on/) assert.ok(!moved.calls.some((x) => x.method === 'POST')) assert.match(r.read().cutover.mergeError, /not the commit the gates ran on/) assert.equal(r.read().cutover.wipe, '2026-11-05') const unmerged = fakeDeps({ handler: (m, p) => (p === '/pulls/7' ? { json: { head: { sha: 'abc123' }, merged: false } } : {}) }) await assert.rejects(c.merge({ pr: '7', sha: 'abc123' }, unmerged), /not merged after the merge call/) const refused = fakeDeps({ handler: (m, p) => (m === 'POST' ? { status: 409, json: { message: 'head changed' } } : { json: { head: { sha: 'abc123' } } }) }) await assert.rejects(c.merge({ pr: '7', sha: 'abc123' }, refused), /409/) await assert.rejects(c.merge({ pr: '7' }, refused), /--sha/) }) test('after records the merge, comments the release on each closed issue once, and is safe to repeat', async () => { const r = rig() fs.writeFileSync(r.state, JSON.stringify({ cutover: { wipe: '2026-11-05', mergeError: 'old', gates: {} } })) const posted = {} const deps = fakeDeps({ handler: (method, p, body) => { if (p === '/pulls/5') return { json: { number: 5, merged: true, merged_at: '2026-11-05T20:00:00Z', body: 'Closes #20\nCloses #22' } } if (p.startsWith('/releases')) return { json: [{ tag_name: 'v1.2.0', created_at: '2026-11-05T20:05:00Z' }, { tag_name: 'v1.1.0', created_at: '2026-10-01T00:00:00Z' }] } if (p === '/issues/20') return { json: { state: 'closed' } } if (p === '/issues/22') return { json: { state: 'open' } } if (method === 'GET' && p === '/issues/20/comments') return { json: (posted[20] || []).map((b) => ({ body: b })) } if (method === 'POST' && p === '/issues/20/comments') { posted[20] = [...(posted[20] || []), body.body] return {} } return { json: [] } }, }) await c.after({ state: r.state, pr: '5' }, deps) await c.after({ state: r.state, pr: '5' }, deps) assert.deepEqual(posted[20], ['Released in v1.2.0 by the wipe cutover (D332).']) assert.equal(deps.calls.filter((x) => x.method === 'POST').length, 1) assert.ok(!deps.calls.some((x) => x.p.startsWith('/issues/22/comments'))) assert.equal(r.read().cutover.merged, true) assert.equal(r.read().cutover.mergeError, undefined) }) test('the Closes list is the PRs merged into edge after the newest edge commit already in main', async () => { const prs = [ { merged: true, base: { ref: 'edge' }, merged_at: '2026-10-20T00:00:00Z', body: 'Closes #1' }, { merged: true, base: { ref: 'edge' }, merged_at: '2026-10-25T00:00:00Z', body: 'Closes #2, fixes #6' }, { merged: true, base: { ref: 'main' }, merged_at: '2026-10-26T00:00:00Z', body: 'Closes #3' }, { merged: false, base: { ref: 'edge' }, merged_at: null, body: 'Closes #4' }, { merged: true, base: { ref: 'edge' }, merged_at: '2026-10-30T00:00:00Z', body: 'Refs #5' }, ] const asked = [] const deps = fakeDeps({ handler: (m, p) => (p.startsWith('/pulls?state=closed') ? { json: prs } : {}), git: (...a) => { asked.push(a.join(' ')) if (a[0] === 'merge-base') return 'deadbeef' if (a[0] === 'log') return '2026-10-22T09:00:00+00:00' return '' }, }) assert.deepEqual(await c.closesSinceCutOff(c.gitea('t', deps), deps), [2, 6]) assert.ok(asked.includes('merge-base origin/main origin/edge')) assert.ok(asked.includes('log -1 --format=%cI deadbeef')) }) test('a PR whose merge commit main already has is not listed again, though merged a moment after the cut-off', async () => { const prs = [ // edge's tip at the last cutover: its merged_at is a second after the merge commit's committer date. { merged: true, base: { ref: 'edge' }, merged_at: '2026-11-05T19:00:01Z', merge_commit_sha: 'shipped1', body: 'Closes #20' }, { merged: true, base: { ref: 'edge' }, merged_at: '2026-11-20T00:00:00Z', merge_commit_sha: 'new1', body: 'Closes #30' }, ] const deps = fakeDeps({ handler: (m, p) => (p.startsWith('/pulls?state=closed') ? { json: prs } : {}), git: (...a) => { if (a[0] === 'merge-base') return 'shipped1' if (a[0] === 'log') return '2026-11-05T19:00:00+00:00' if (a[0] === 'branch') return a[3] === 'shipped1' ? ' origin/main' : '' return '' }, }) assert.deepEqual(await c.closesSinceCutOff(c.gitea('t', deps), deps), [30]) }) test('rust-carbon is started again only when it was running at the start and is offline now, and nothing else is touched', async () => { const dir = tmp() const powered = [] const mkPanel = (state) => () => ({ servers: async () => ({ 'rust-carbon': 'abc', 'rust-oxide': 'def' }), state: async () => state, power: async (id, signal) => powered.push([id, signal]), }) await c.rigNote({ work: dir }, { panel: mkPanel('running') }) assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'rig-carbon.json'), 'utf8')), { name: 'rust-carbon', state: 'running' }) await c.rigRestore({ work: dir }, { panel: mkPanel('running') }) await c.rigRestore({ work: dir }, { panel: mkPanel('starting') }) assert.deepEqual(powered, []) await c.rigRestore({ work: dir }, { panel: mkPanel('offline') }) assert.deepEqual(powered, [['abc', 'start']]) // It was off at the start: left off. await c.rigNote({ work: dir }, { panel: mkPanel('offline') }) await c.rigRestore({ work: dir }, { panel: mkPanel('offline') }) assert.equal(powered.length, 1) }) // ---- the workflow and the scripts agree const yml = fs.readFileSync(path.join(__dirname, '..', '..', '.gitea', 'workflows', 'cutover.yml'), 'utf8') const src = fs.readFileSync(path.join(__dirname, 'cutover.js'), 'utf8') function stepsOf(text) { return text .split(/\n {6}- (?=name:|uses:)/) .slice(1) .map((b) => ({ id: (/^\s+id: (\w+)/m.exec(b) || [])[1], command: (/cutover\.js (\S+)/.exec(b) || /drill\.js (\S+)/.exec(b) || [])[1], script: /drill\.js/.test(b) && !/cutover\.js/.test(b) ? 'drill' : 'cutover', text: b })) } const FUNCTIONS = { window: 'window_', pending: 'pending', gates: 'gates', merge: 'merge', after: 'after' } function written(command) { const m = new RegExp(`\\n(?:async )?function ${FUNCTIONS[command]}\\(`).exec(src) assert.ok(m, `cutover.js has no function for ${command}`) const body = src.slice(m.index, src.indexOf('\n}\n', m.index)) return new Set([...body.matchAll(/output\([^,]+,\s*'(\w+)'/g)].map((x) => x[1])) } test('every steps..outputs. the workflow reads is written by the command that step runs', () => { const steps = stepsOf(yml) const used = [...yml.matchAll(/steps\.(\w+)\.outputs\.(\w+)/g)].map((m) => [m[1], m[2]]) assert.ok(used.length >= 8) for (const [id, name] of used) { const step = steps.find((s) => s.id === id) assert.ok(step, `steps.${id} is not a step of cutover.yml`) assert.equal(step.script, 'cutover', `steps.${id}.outputs.${name}: ${id} runs drill.js, which writes no output the workflow reads`) assert.ok(written(step.command).has(name), `cutover.js ${step.command} never writes output ${name} (read as steps.${id}.outputs.${name})`) } }) test('and the outputs the logic depends on are all read by the workflow', () => { const steps = stepsOf(yml) const needed = { window: ['open'], pending: ['run'], gates: ['live', 'merge', 'pr', 'sha'], merge: ['merged'] } for (const [command, names] of Object.entries(needed)) { const ids = steps.filter((s) => s.script === 'cutover' && s.command === command && s.id).map((s) => s.id) assert.ok(ids.length > 0, `no step with an id runs cutover.js ${command}`) const read = new Set([...yml.matchAll(/steps\.(\w+)\.outputs\.(\w+)/g)].filter((m) => ids.includes(m[1])).map((m) => m[2])) for (const n of names) { assert.ok(read.has(n), `cutover.yml never reads ${n} from the ${command} step`) assert.ok(written(command).has(n), `cutover.js ${command} no longer writes ${n}`) } } }) test('the code checked out is edge, except on a dry dispatch that names code_ref', () => { const refs = yml.split('\n').filter((l) => /^\s*ref:/.test(l)).map((l) => l.trim()) assert.deepEqual(refs, [ "ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'false' && github.event.inputs.code_ref || 'edge' }}", ]) }) test('RUNICNPC_TOKEN is in the merge step alone, and a dispatch is dry unless dry_run is exactly false', () => { const uses = yml.split('\n').filter((l) => !l.trim().startsWith('#') && l.includes('RUNICNPC_TOKEN')) assert.equal(uses.length, 1) const merge = stepsOf(yml).find((s) => s.text.split('\n').some((l) => !l.trim().startsWith('#') && l.includes('RUNICNPC_TOKEN'))) assert.equal(merge.id, 'merge') assert.match(merge.text, /MERGE_TOKEN: \$\{\{ secrets\.RUNICNPC_TOKEN \}\}/) const code = yml.split('\n').filter((l) => !l.trim().startsWith('#')).join('\n') assert.ok(!/dry_run\s*[=!]=\s*'true'/.test(code), 'dry_run must be compared with false, never true') assert.ok((code.match(/dry_run != 'false'/g) || []).length >= 6) for (const key of ['LC_ALL: C', "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: '1'"]) assert.equal(code.split(key).length - 1, 2, `${key} on both static halves`) assert.match(code, /timeout-minutes: 240/) })