Files
Module-Rust/server/test/nextWipe.test.js
wtclaude 0670341198 feat(rust): slash commands and the next wipe, server half (phase 16)
Five read-only commands registered with api.registerSlashCommands:
/status, /wipe, /top, /online and /clan (D126). Every refusal is private,
and any answer narrower than public (online names, a clan roster) goes
to the caller alone (D127). No command asks a sidecar.

The next wipe (D128, D130): six nullable columns on rust_servers, a pure
nextWipe(row, now) with the zone arithmetic through Intl, computed on
every read. The public server shape gains nextWipe; the admin shape
gains the stored schedule; PUT /admin/rust/servers/:id takes the six
fields and writes them only when wipeRule is present.

server/commands joins ci/bundle.json, which checkBundle caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-25 13:29:59 -05:00

137 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── The next wipe (phase 16, D130) ────────────────────────────────────────
//
// Pure arithmetic, so no core and no database. The DST edges are here rather
// than in the walk (§32.3 step 5): waiting for a clock change is not a test.
const test = require('node:test')
const assert = require('node:assert')
const { nextWipe, validateSchedule, instantOf, parseDay, weekdayOf, isZone } = require('../model/servers/nextWipe')
const at = (iso) => Date.parse(iso)
const day = (iso) => parseDay(iso)
test('the forced wipe is the first Thursday, 19:00 UK time — 18:00 UTC in summer', () => {
// The verified fact the plan rests on: Thursday 1 October 2026, 18:00 UTC.
const next = nextWipe({ wipeRule: 'forced' }, at('2026-09-25T12:00:00Z'))
assert.deepStrictEqual(next, { at: '2026-10-01T18:00:00.000Z', source: 'forced' })
// In winter the UK clock is UTC, and 19:00 is 19:00Z.
const winter = nextWipe({ wipeRule: 'forced' }, at('2026-11-10T00:00:00Z'))
assert.deepStrictEqual(winter, { at: '2026-12-03T19:00:00.000Z', source: 'forced' })
})
test('a forced wipe that has just happened names next month’s', () => {
const next = nextWipe({ wipeRule: 'forced' }, at('2026-10-01T18:00:00Z'))
assert.strictEqual(next.at, '2026-11-05T19:00:00.000Z')
})
test('`none` forecasts nothing, not even the forced wipe', () => {
assert.strictEqual(nextWipe({ wipeRule: 'none' }, at('2026-09-25T12:00:00Z')), null)
assert.strictEqual(nextWipe({}, at('2026-09-25T12:00:00Z')), null)
// An unknown rule word narrows to none rather than guessing.
assert.strictEqual(nextWipe({ wipeRule: 'daily' }, at('2026-09-25T12:00:00Z')), null)
})
test('a weekly rule is the earlier of its own day and the forced wipe', () => {
const weekly = { wipeRule: 'weekly', wipeDay: 4, wipeTime: '14:00', wipeTz: 'America/Chicago' }
// Friday 25 Sep: the next Thursday is 1 Oct, 14:00 CDT = 19:00Z — AFTER the
// forced wipe at 18:00Z the same day, so the forced one is next.
assert.deepStrictEqual(nextWipe(weekly, at('2026-09-25T12:00:00Z')), { at: '2026-10-01T18:00:00.000Z', source: 'forced' })
// Once that has passed, the rule's own Thursday comes first.
assert.deepStrictEqual(nextWipe(weekly, at('2026-10-01T18:30:00Z')), { at: '2026-10-01T19:00:00.000Z', source: 'rule' })
assert.deepStrictEqual(nextWipe(weekly, at('2026-10-01T19:00:00Z')), { at: '2026-10-08T19:00:00.000Z', source: 'rule' })
})
test('a biweekly rule wipes on its anchor’s weeks only', () => {
const biweekly = { wipeRule: 'biweekly', wipeDay: 1, wipeTime: '20:00', wipeTz: 'Europe/London', wipeAnchor: '2026-09-14' }
// Mon 14 Sep is on, 21 Sep off, 28 Sep on (20:00 BST = 19:00Z).
assert.deepStrictEqual(nextWipe(biweekly, at('2026-09-15T00:00:00Z')), { at: '2026-09-28T19:00:00.000Z', source: 'rule' })
// An anchor in the future still defines the parity.
assert.deepStrictEqual(
nextWipe({ ...biweekly, wipeAnchor: '2026-12-07' }, at('2026-09-15T00:00:00Z')),
{ at: '2026-09-28T19:00:00.000Z', source: 'rule' },
)
})
test('a one-off date in the future is the next wipe, before or after the computed one', () => {
const forced = { wipeRule: 'forced' }
const now = at('2026-09-25T12:00:00Z')
// Before the computed wipe: an extra wipe.
assert.deepStrictEqual(nextWipe({ ...forced, wipeOnceAt: '2026-09-27T17:00:00Z' }, now), { at: '2026-09-27T17:00:00.000Z', source: 'once' })
// After it: a delay — the computed 1 Oct wipe is skipped.
assert.deepStrictEqual(nextWipe({ ...forced, wipeOnceAt: new Date('2026-10-03T17:00:00Z') }, now), { at: '2026-10-03T17:00:00.000Z', source: 'once' })
// Past: ignored, and the rule answers again.
assert.deepStrictEqual(nextWipe({ ...forced, wipeOnceAt: '2026-09-20T17:00:00Z' }, now), { at: '2026-10-01T18:00:00.000Z', source: 'forced' })
// Under `none` a stated date is still a forecast.
assert.deepStrictEqual(nextWipe({ wipeRule: 'none', wipeOnceAt: '2026-09-27T17:00:00Z' }, now), { at: '2026-09-27T17:00:00.000Z', source: 'once' })
})
test('a broken rule still answers the forced wipe, and never throws', () => {
const now = at('2026-09-25T12:00:00Z')
const broken = { wipeRule: 'weekly', wipeDay: 4, wipeTime: '25:00', wipeTz: 'Mars/Olympus' }
assert.deepStrictEqual(nextWipe(broken, now), { at: '2026-10-01T18:00:00.000Z', source: 'forced' })
assert.strictEqual(nextWipe(null, now), null)
})
// ── The DST edges, both zones, both directions (reading 6) ────────────────
test('Europe/London: a skipped hour moves forward, a repeated hour takes the first', () => {
// Spring 2027: 29 Mar 01:00 GMT → 02:00 BST. 01:30 does not exist.
assert.strictEqual(new Date(instantOf(day('2027-03-28'), 1, 30, 'Europe/London')).toISOString(), '2027-03-28T01:30:00.000Z')
// Autumn 2026: 25 Oct 02:00 BST → 01:00 GMT. 01:30 happens twice; the first is BST.
assert.strictEqual(new Date(instantOf(day('2026-10-25'), 1, 30, 'Europe/London')).toISOString(), '2026-10-25T00:30:00.000Z')
// Either side of the change, ordinary.
assert.strictEqual(new Date(instantOf(day('2026-10-24'), 19, 0, 'Europe/London')).toISOString(), '2026-10-24T18:00:00.000Z')
assert.strictEqual(new Date(instantOf(day('2026-10-26'), 19, 0, 'Europe/London')).toISOString(), '2026-10-26T19:00:00.000Z')
})
test('America/Chicago: a skipped hour moves forward, a repeated hour takes the first', () => {
// Spring 2027: 14 Mar 02:00 CST → 03:00 CDT. 02:30 does not exist → 03:30 CDT = 08:30Z.
assert.strictEqual(new Date(instantOf(day('2027-03-14'), 2, 30, 'America/Chicago')).toISOString(), '2027-03-14T08:30:00.000Z')
// Autumn 2026: 1 Nov 02:00 CDT → 01:00 CST. 01:30 twice; the first is CDT = 06:30Z.
assert.strictEqual(new Date(instantOf(day('2026-11-01'), 1, 30, 'America/Chicago')).toISOString(), '2026-11-01T06:30:00.000Z')
})
test('a weekly rule across the autumn change keeps its wall-clock time', () => {
const weekly = { wipeRule: 'weekly', wipeDay: 0, wipeTime: '14:00', wipeTz: 'America/Chicago' }
// Sun 25 Oct 14:00 CDT = 19:00Z; Sun 1 Nov 14:00 CST = 20:00Z.
assert.strictEqual(nextWipe(weekly, at('2026-10-24T00:00:00Z')).at, '2026-10-25T19:00:00.000Z')
assert.strictEqual(nextWipe(weekly, at('2026-10-26T00:00:00Z')).at, '2026-11-01T20:00:00.000Z')
})
test('the calendar helpers', () => {
assert.strictEqual(weekdayOf(day('2026-10-01')), 4)
assert.strictEqual(weekdayOf(day('1969-12-31')), 3)
assert.strictEqual(parseDay('2026-02-30'), null)
assert.strictEqual(parseDay('26-02-01'), null)
assert.strictEqual(isZone('Europe/London'), true)
assert.strictEqual(isZone('Europe/Londn'), false)
assert.strictEqual(isZone(''), false)
})
test('validation names every problem, and a one-off date must be in the future', () => {
const now = at('2026-09-25T12:00:00Z')
assert.deepStrictEqual(validateSchedule({ wipeRule: 'none' }, now), [])
assert.deepStrictEqual(validateSchedule({ wipeRule: 'forced' }, now), [])
assert.deepStrictEqual(validateSchedule({ wipeRule: 'weekly', wipeDay: 4, wipeTime: '19:00', wipeTz: 'Europe/London' }, now), [])
assert.strictEqual(validateSchedule({ wipeRule: 'monthly' }, now).length, 1)
assert.strictEqual(validateSchedule({ wipeRule: 'weekly' }, now).length, 3)
assert.match(validateSchedule({ wipeRule: 'weekly', wipeDay: 4, wipeTime: '19:00', wipeTz: 'UK' }, now)[0], /time zone/)
// The anchor must be a wipe on the rule: a Tuesday anchor for a Monday rule is refused.
const bi = { wipeRule: 'biweekly', wipeDay: 1, wipeTime: '20:00', wipeTz: 'Europe/London' }
assert.match(validateSchedule(bi, now)[0], /date of one wipe/)
assert.match(validateSchedule({ ...bi, wipeAnchor: '2026-09-15' }, now)[0], /day of the week/)
assert.deepStrictEqual(validateSchedule({ ...bi, wipeAnchor: '2026-09-14' }, now), [])
assert.match(validateSchedule({ wipeRule: 'forced', wipeOnceAt: '2026-09-24T00:00:00Z' }, now)[0], /future/)
assert.match(validateSchedule({ wipeRule: 'forced', wipeOnceAt: 'soon' }, now)[0], /not a date/)
assert.deepStrictEqual(validateSchedule({ wipeRule: 'forced', wipeOnceAt: null }, now), [])
})