Files
Module-Rust/client/test/format.test.js
wtclaude b10f11b057
Some checks failed
PR Checks / client-build (pull_request) Successful in 18s
PR Checks / frozen-manifest (pull_request) Failing after 56s
PR Checks / server-tests (pull_request) Successful in 7m45s
fix(rust): protocol 13 step 2 — expiry, plugin loads, the loading hold, NPC names, the link fleet (F2 F5 F6 F7 F8 F13 F14)
The module's half of PLAN_FIXES §6 step 2 (decisions D181-D185, docs#288).

- F13/F14 (D170, D183): `world.expired`, recognisable from protocol 13 by its
  `what`, is handed to core as the resource the zone step ledgered
  (`world`, `<serverId>:<id>`) through ctx.events.expired, which records it
  `expired`. coreApi moves to ^1.11.0 (website#209).
- F8 (D184): `plugin.loaded` / `plugin.unloaded` mark the permission sync dirty
  when the plugin added or removed permissions, so an unresolved grant lands on
  the next tick instead of the fifteen-minute audit.
- Catalogue: plugin.loaded/unloaded, world.expired and lease.expired are staff
  kinds. The last two were never classified (default deny kept them off public
  pages); the test now covers every event kind through protocol 13.
- F7: permission and title pushes hold while the stored hello says
  `worldReady: false` (a human's "sync now" does not); a failed or refused
  permission sync now logs at warn.
- F2 (D185): the killfeed names an NPC attacker — a family (Scientist, Bandit
  guard, Bradley APC…) or the prefab without its variant digits (wolf2 → Wolf).
- F5/F6: a link code is asked of the servers that minted one in the last six
  minutes first, then of the rest, each group in parallel; "unsure" only when
  one of the minting servers is unreachable.
- D182: the admin server list carries the ZoneManager helper's state from the
  hello, and the servers page says what a missing or failed helper costs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 21:35:46 -05:00

143 lines
6.5 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.

// ── Formatting ────────────────────────────────────────────────────────────
//
// Small functions, and the tests are small too — but three of them guard claims
// that would otherwise be made by a page that looks fine: an unknown duration
// rendered as zero, a timestamp in the wrong unit, and "in 0 seconds".
//
// Locale-dependent output is asserted loosely on purpose. `Intl` formats to the
// RUNNER's locale, and a test pinned to "3 minutes ago" would be a test that
// fails on a machine set to French while the page it describes is correct.
import test from 'node:test'
import assert from 'node:assert/strict'
import { ago, attacker, clock, contrastInk, count, day, duration, nextWipe, prefab, shortId } from '../src/lib/format.js'
const NOW = Date.parse('2026-09-16T12:00:00Z')
test('a relative time picks the unit that fits', () => {
assert.match(ago(NOW - 3 * 60_000, NOW), /3/)
assert.match(ago(NOW - 5 * 3600_000, NOW), /5/)
assert.match(ago(NOW - 3 * 86400_000, NOW), /3/)
})
test('"just now" rather than "in 0 seconds"', () => {
// What `numeric: 'auto'` produces under a minute is not what anybody means,
// and a feed row a few seconds old is the commonest row on the page.
assert.equal(ago(NOW, NOW), 'just now')
assert.equal(ago(NOW - 10_000, NOW), 'just now')
})
test('both time shapes this module serves are accepted', () => {
// `updatedAt` is an ISO string the model produced; an event's `t` is the
// millisecond stamp the plugin put on the frame. A helper that took only one
// would be a helper every caller has to remember the type for.
assert.equal(ago('2026-09-16T11:57:00.000Z', NOW), ago(NOW - 3 * 60_000, NOW))
})
test('a missing time is "never", not the epoch', () => {
assert.equal(ago(null), 'never')
assert.equal(ago(undefined), 'never')
assert.equal(ago(''), 'never')
assert.equal(day(null), 'unknown')
})
test('an unknown duration is a dash, and a short one keeps its seconds', () => {
// The distinction the plugin makes and this must not lose: `sessionSec` is
// ABSENT for a player who was already on when it loaded, so zero and unknown
// arrive at the same function and must not render the same way.
assert.equal(duration(null), '—')
assert.equal(duration(0), '—')
assert.equal(duration(40), '40s')
assert.equal(duration(90), '2m')
assert.equal(duration(3720), '1h 2m')
assert.equal(duration(7200), '2h')
})
test('a prefab reads as words, without a lookup table', () => {
assert.equal(prefab('rifle.ak'), 'rifle ak')
assert.equal(prefab('scientistnpc_full_any'), 'scientistnpc full any')
assert.equal(prefab(null), '')
})
test('a steam id is shortened without pretending to be a name', () => {
assert.equal(shortId('76561198000000001'), '…000001')
assert.equal(shortId(''), '')
})
test('a count that is not a number is zero, never NaN on the page', () => {
assert.equal(count(undefined), '0')
assert.equal(count(null), '0')
})
test("a feed row from another day carries its date, not just a time", () => {
// Found by the page walk: with the feed filtered to the previous wipe, three
// events from six weeks ago rendered as `02:03 PM` and read as this afternoon.
// Today's rows stay bare, because a killfeed of today's fights does not want
// the date on every line.
// Asserted against `Intl` rather than against a literal: a 12-hour locale puts
// letters in a bare time ("05:30 AM"), so "has letters in it" is not the test —
// "is exactly the time, and nothing else" is.
const time = (at) => new Date(at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
const todayAt = NOW - 90 * 60_000
assert.equal(clock(todayAt, NOW), time(todayAt))
const olderAt = NOW - 46 * 86400_000
assert.ok(clock(olderAt, NOW).endsWith(time(olderAt)))
assert.ok(clock(olderAt, NOW).length > time(olderAt).length, 'an older row carries no date')
// Yesterday counts as another day even when it is only a few hours back — the
// boundary is the calendar, not a duration, because that is what a reader
// means by "what time was that".
const lateLastNight = Date.parse('2026-09-15T23:50:00')
const earlyToday = Date.parse('2026-09-16T00:20:00')
assert.ok(clock(lateLastNight, earlyToday).length > time(lateLastNight).length)
})
test('the next wipe: the reader’s own clock, how far away, and whether it moved', () => {
const now = Date.parse('2026-09-25T12:00:00Z')
const forced = nextWipe({ at: '2026-10-01T18:00:00.000Z', source: 'forced' }, now)
assert.match(forced, /\(in 6 days\)$/)
assert.ok(!forced.includes('rescheduled'))
assert.match(nextWipe({ at: '2026-09-27T17:00:00.000Z', source: 'once' }, now), /\(in 2 days\) — rescheduled$/)
// No schedule is no line, not "unknown": the page leaves the part out.
assert.equal(nextWipe(null, now), null)
assert.equal(nextWipe({ at: 'soon', source: 'rule' }, now), null)
})
test('a title chip’s ink is whichever of black and white reads on its colour', () => {
assert.equal(contrastInk('#ffff00'), '#000000')
assert.equal(contrastInk('#FFAA55'), '#000000')
assert.equal(contrastInk('#1a1a8c'), '#ffffff')
assert.equal(contrastInk('#ff0000'), '#000000')
assert.equal(contrastInk('red'), '#000000', 'not a hex colour: black, on the caller’s fallback')
})
test('an NPC attacker reads as a name: a family, or the prefab without its variant (F2, D185)', () => {
// The first walk's "killed by wolf2".
assert.equal(attacker('wolf2'), 'Wolf')
assert.equal(attacker('wolf'), 'Wolf')
assert.equal(attacker('boar'), 'Boar')
assert.equal(attacker('polarbear'), 'Polar bear')
assert.equal(attacker('scientistnpc_full_any'), 'Scientist')
assert.equal(attacker('scientistnpc_roam'), 'Scientist')
assert.equal(attacker('scientistnpc_heavy'), 'Heavy scientist')
assert.equal(attacker('npc_bandit_guard'), 'Bandit guard')
assert.equal(attacker('bradleyapc'), 'Bradley APC')
assert.equal(attacker('patrolhelicopter'), 'Patrol helicopter')
assert.equal(attacker('autoturret_deployed'), 'Auto turret')
assert.equal(attacker('sam_site_turret_deployed'), 'SAM site')
// Anything unknown still reads as words, capitalised, with no suffix or variant.
assert.equal(attacker('some_new_beast3'), 'Some new beast')
assert.equal(attacker('spikes.floor.deployed'), 'Spikes floor')
assert.equal(attacker(null), '')
assert.equal(attacker(''), '')
})
test('a weapon keeps the light touch — attacker() is for what did the killing', () => {
assert.equal(prefab('rifle.ak'), 'rifle ak')
})