@bongos/core 1.19.539 → 1.19.540
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +27 -22
- package/docs/copy-inventory.md +38 -36
- package/docs/copy-registry.json +63 -45
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/hall-kit.js +105 -6
- package/modules/hall-ui/public/profile-charts.js +39 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +8 -1
- package/modules/hall-ui/public/style.css +31 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/routes/me.js +42 -0
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +4 -1
- package/tests/hall_kit_charts.mjs +75 -0
- package/tests/hall_profile_world.mjs +4 -1
- package/tests/me_ship_days_bound.mjs +76 -0
|
@@ -1988,6 +1988,37 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
1988
1988
|
.kit-chart__empty { font-size: 12px; color: var(--ink-faint); }
|
|
1989
1989
|
.kit-chart__foot { font-size: 11.5px; color: var(--ink-faint); margin-top: 4px; }
|
|
1990
1990
|
|
|
1991
|
+
/* The contribution calendar — OTBKit.calendarGridHtml (task 1003572), the hall's
|
|
1992
|
+
THIRD chart family. A year grid of small cells, banded by ship count 0..5. Theme-
|
|
1993
|
+
aware by construction: every band is a color-mix off --accent and --surface-2, both
|
|
1994
|
+
theme tokens, so light and dark read without a second palette and level 5 is the
|
|
1995
|
+
full accent in either. The grid is wide, so .kit-cal__scroll holds it in its OWN
|
|
1996
|
+
horizontal strip — the document never scrolls sideways (hall-ui/CLAUDE.md). */
|
|
1997
|
+
.kit-cal { display: flex; flex-direction: column; gap: 6px; --kit-cal-cell: 11px; }
|
|
1998
|
+
.kit-cal__scroll { overflow-x: auto; padding-bottom: 2px; }
|
|
1999
|
+
.kit-cal__grid {
|
|
2000
|
+
display: grid;
|
|
2001
|
+
grid-template-rows: repeat(7, var(--kit-cal-cell));
|
|
2002
|
+
grid-auto-flow: column;
|
|
2003
|
+
grid-auto-columns: var(--kit-cal-cell);
|
|
2004
|
+
gap: 3px;
|
|
2005
|
+
width: max-content; /* intrinsic width; the scroll strip, not the document, holds it */
|
|
2006
|
+
}
|
|
2007
|
+
.kit-cal__cell {
|
|
2008
|
+
width: var(--kit-cal-cell); height: var(--kit-cal-cell);
|
|
2009
|
+
border-radius: 2px;
|
|
2010
|
+
background: var(--surface-2);
|
|
2011
|
+
}
|
|
2012
|
+
.kit-cal__cell[data-level="1"] { background: color-mix(in srgb, var(--accent) 25%, var(--surface-2)); }
|
|
2013
|
+
.kit-cal__cell[data-level="2"] { background: color-mix(in srgb, var(--accent) 45%, var(--surface-2)); }
|
|
2014
|
+
.kit-cal__cell[data-level="3"] { background: color-mix(in srgb, var(--accent) 65%, var(--surface-2)); }
|
|
2015
|
+
.kit-cal__cell[data-level="4"] { background: color-mix(in srgb, var(--accent) 82%, var(--surface-2)); }
|
|
2016
|
+
.kit-cal__cell[data-level="5"] { background: var(--accent); }
|
|
2017
|
+
.kit-cal__cell--void { background: transparent; }
|
|
2018
|
+
.kit-cal__key { display: flex; align-items: center; gap: 3px; font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-faint); }
|
|
2019
|
+
.kit-cal__key .kit-cal__cell { --kit-cal-cell: 10px; }
|
|
2020
|
+
.kit-cal__key-label { margin: 0 2px; }
|
|
2021
|
+
|
|
1991
2022
|
.kit-spark { width: 100%; height: 44px; display: block; }
|
|
1992
2023
|
/* The stroke/fill here are the FALLBACK: sparklineHtml writes the caller's
|
|
1993
2024
|
colour inline, because the Curve spends a different accent per chart. */
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.540",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.540",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.540",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -45,6 +45,34 @@ const seams = require('../../module-seams');
|
|
|
45
45
|
// `lifecycle` kernel PORT (lc()); the grade-TREND read (builderRollingAvgGrade)
|
|
46
46
|
// stays core db (the sessions BFG also consumes it — kept on the doorway).
|
|
47
47
|
const lc = () => seams.resolveOptional('lifecycle');
|
|
48
|
+
|
|
49
|
+
// boundShipDaysToYear(rows, todayStr) — the last ~year of a builder's ship-day
|
|
50
|
+
// counts, for the hall's contribution calendar (task 1003572). `rows` is
|
|
51
|
+
// computeStreak's `ship_day_counts` ([{ local_date:'YYYY-MM-DD', count }]) ascending
|
|
52
|
+
// in the builder's own timezone; `todayStr` is that same day. A calendar shows a
|
|
53
|
+
// year, and /me is the hall's busiest boot read, so an unbounded array is the exact
|
|
54
|
+
// over-fetch criterion legibility-sort-and-staleness names — the bound is the point.
|
|
55
|
+
//
|
|
56
|
+
// The cutoff is computed on the YYYY-MM-DD-as-UTC convention the streak math already
|
|
57
|
+
// uses (Date.UTC on the parsed parts), never through the server's local clock, so it
|
|
58
|
+
// cannot drift a day the way `new Date('2026-09-03')` would. 371 days = 53 whole
|
|
59
|
+
// weeks, so the grid the calendar draws is always full-width with no partial column.
|
|
60
|
+
const YMD_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
61
|
+
function boundShipDaysToYear(rows, todayStr) {
|
|
62
|
+
if (!Array.isArray(rows) || rows.length === 0) return [];
|
|
63
|
+
const toOrd = (ymd) => {
|
|
64
|
+
const m = YMD_RE.exec(String(ymd || ''));
|
|
65
|
+
if (!m) return null;
|
|
66
|
+
return Math.floor(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) / 86400000);
|
|
67
|
+
};
|
|
68
|
+
const todayOrd = toOrd(todayStr);
|
|
69
|
+
if (todayOrd == null) return rows.slice(-371); // no today anchor → last 371 rows, still bounded
|
|
70
|
+
const cutoff = todayOrd - 370;
|
|
71
|
+
return rows.filter((r) => {
|
|
72
|
+
const o = toOrd(r && r.local_date);
|
|
73
|
+
return o != null && o >= cutoff && o <= todayOrd;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
48
76
|
// The dev-box "has this builder ever connected a box?" signal, read through the
|
|
49
77
|
// kernel seam instead of a direct `require('../boxes')` (a forbidden core→module
|
|
50
78
|
// edge once dev-box moved to modules/dev-box/, ADR 0083 / BV1.R43). The dev-box
|
|
@@ -302,6 +330,15 @@ module.exports = function buildMeRouter() {
|
|
|
302
330
|
started_on: streakInfo.current_started_on,
|
|
303
331
|
timezone: streakInfo.timezone,
|
|
304
332
|
today: streakInfo.today,
|
|
333
|
+
// The per-day ship counts computeStreak already produced and this block
|
|
334
|
+
// used to drop (task 1003572, criterion legibility-series-are-drawn): the
|
|
335
|
+
// hall's contribution calendar draws them. BOUNDED to roughly a year — a
|
|
336
|
+
// calendar shows a year, and an unbounded array would grow without limit on
|
|
337
|
+
// the busiest boot read the hall has. `ship_day_counts` is ascending in the
|
|
338
|
+
// builder's own timezone ({local_date:'YYYY-MM-DD', count:1..5}); the cutoff
|
|
339
|
+
// is computed on the same YYYY-MM-DD-as-UTC convention the streak math uses,
|
|
340
|
+
// never through the request's local clock.
|
|
341
|
+
ship_days: boundShipDaysToYear(streakInfo.ship_day_counts, streakInfo.today),
|
|
305
342
|
},
|
|
306
343
|
grades: {
|
|
307
344
|
recent: recentGrades,
|
|
@@ -531,3 +568,8 @@ module.exports = function buildMeRouter() {
|
|
|
531
568
|
|
|
532
569
|
return router;
|
|
533
570
|
};
|
|
571
|
+
|
|
572
|
+
// Exported for tests only: the "bounded to a year" + timezone-safe cutoff behind the
|
|
573
|
+
// contribution calendar (task 1003572). The route mounts the router; this is the one
|
|
574
|
+
// pure function inside it whose correctness is worth pinning without a DB.
|
|
575
|
+
module.exports.boundShipDaysToYear = boundShipDaysToYear;
|
package/src/module-api.js
CHANGED
|
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
49
49
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
50
50
|
// the entry to that file. Look for a version's history there, not here.
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
|
-
const CORE_VERSION = '1.19.
|
|
52
|
+
const CORE_VERSION = '1.19.540'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
53
53
|
|
|
54
54
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
55
55
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -256,7 +256,9 @@ test('the kit is the ONLY thing in the hall that draws a bar series', () => {
|
|
|
256
256
|
const stamped = [];
|
|
257
257
|
for (const f of fs.readdirSync(HALL)) {
|
|
258
258
|
if (!/\.(html|js)$/.test(f) || f === OWNER) continue;
|
|
259
|
-
|
|
259
|
+
// `cal` joins the family (task 1003572): the contribution calendar is stamped by
|
|
260
|
+
// OTBKit.calendarGridHtml, so a `kit-cal` class in any other file is a hand-roll.
|
|
261
|
+
if (/class="kit-(?:bars|spark|plot|cal)[\s"_-]/.test(read(f))) stamped.push(f);
|
|
260
262
|
}
|
|
261
263
|
assert.deepEqual(stamped, [],
|
|
262
264
|
`${stamped.join(', ')} writes the kit's own chart classes by hand — call `
|
|
@@ -317,6 +319,7 @@ test('the three bar copies are gone, and their class families with them', () =>
|
|
|
317
319
|
assert.match(read('hall-render.js'), /OTBKit\.barSeriesHtml\(/, 'the Curve\'s kinds draw through the kit');
|
|
318
320
|
assert.match(read('watch.js'), /OTBKit\.barSeriesHtml\(/, 'Watch\'s trend draws through the kit');
|
|
319
321
|
assert.match(read('profile-charts.js'), /OTBKit\.barSeriesHtml\(/, 'the craft mix draws through the kit');
|
|
322
|
+
assert.match(read('profile-charts.js'), /OTBKit\.calendarGridHtml\(/, 'the contribution calendar draws through the kit (task 1003572)');
|
|
320
323
|
assert.match(read('hall-render.js'), /OTBKit\.sparklineHtml\(/, 'and the Curve\'s line does too');
|
|
321
324
|
});
|
|
322
325
|
|
|
@@ -548,3 +548,78 @@ test('RUN: the timeline refuses to draw rather than drawing an empty axis', asyn
|
|
|
548
548
|
assert.doesNotMatch(card.html, />C</, 'the unstarted version is not a bar');
|
|
549
549
|
assert.match(card.html, /1 version with no start date is not shown/, 'the foot says so');
|
|
550
550
|
});
|
|
551
|
+
|
|
552
|
+
// ---- the contribution calendar (task 1003572) -------------------------------
|
|
553
|
+
//
|
|
554
|
+
// The hall's third chart family: a year grid, banded by count, read by per-cell
|
|
555
|
+
// title + an aria summary. These pin the arithmetic (banding, the 5-cap), the
|
|
556
|
+
// timezone trap the task names, and the empty-history contract.
|
|
557
|
+
|
|
558
|
+
test('CAL: a day lands on the cell its own YYYY-MM-DD names, banded by count', () => {
|
|
559
|
+
const html = kit.calendarGridHtml([
|
|
560
|
+
{ local_date: '2026-09-01', count: 2 },
|
|
561
|
+
{ local_date: '2026-09-03', count: 5 },
|
|
562
|
+
], { today: '2026-09-03', weeks: 6 });
|
|
563
|
+
assert.match(html, /2026-09-03 · 5 ships/);
|
|
564
|
+
assert.match(html, /2026-09-01 · 2 ships/);
|
|
565
|
+
assert.match(html, /data-level="5"/, 'a 5-ship day hits the top band');
|
|
566
|
+
assert.match(html, /data-level="2"/);
|
|
567
|
+
assert.match(html, /· no ships/, 'quiet days are titled too, not blank');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('CAL: the band caps at 5 — the same anti-gaming cap streak.js applies', () => {
|
|
571
|
+
// count can only be 1..5 from the server, but the primitive must not emit a 6th
|
|
572
|
+
// band if a caller ever over-counts.
|
|
573
|
+
const html = kit.calendarGridHtml([{ local_date: '2026-09-03', count: 9 }], { today: '2026-09-03', weeks: 2 });
|
|
574
|
+
assert.match(html, /2026-09-03 · 9 ships/, 'the title reports the real number');
|
|
575
|
+
assert.match(html, /data-level="5"/, 'but the band is clamped to 5');
|
|
576
|
+
assert.doesNotMatch(html, /data-level="[6-9]"/);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test('CAL: the aria summary states the figure in words for a non-picture reader', () => {
|
|
580
|
+
const html = kit.calendarGridHtml([
|
|
581
|
+
{ local_date: '2026-09-01', count: 2 },
|
|
582
|
+
{ local_date: '2026-09-02', count: 1 },
|
|
583
|
+
], { today: '2026-09-03' });
|
|
584
|
+
assert.match(html, /aria-label="Ships per day over the last year: 3 ships across 2 days\./);
|
|
585
|
+
assert.match(html, /role="img"/);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('CAL: singular grammar — one ship, one day', () => {
|
|
589
|
+
const html = kit.calendarGridHtml([{ local_date: '2026-09-03', count: 1 }], { today: '2026-09-03' });
|
|
590
|
+
assert.match(html, /2026-09-03 · 1 ship"/, "no plural s on one ship");
|
|
591
|
+
assert.match(html, /1 ship across 1 day\./);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('CAL: no timezone shift — the cell is fixed by the date string, not the runner clock', () => {
|
|
595
|
+
// The trap the task names: `new Date("2026-09-03")` is UTC-midnight and shifts a
|
|
596
|
+
// day west of Greenwich. The primitive uses Date.UTC on the parsed parts, so the
|
|
597
|
+
// title for a date is that date regardless of process.env.TZ.
|
|
598
|
+
const days = [{ local_date: '2026-01-01', count: 3 }];
|
|
599
|
+
const run = () => kit.calendarGridHtml(days, { today: '2026-01-07', weeks: 3 });
|
|
600
|
+
const saved = process.env.TZ;
|
|
601
|
+
try {
|
|
602
|
+
process.env.TZ = 'Pacific/Kiritimati'; // UTC+14, the worst case east
|
|
603
|
+
const a = run();
|
|
604
|
+
process.env.TZ = 'Pacific/Pago_Pago'; // UTC-11, the worst case west
|
|
605
|
+
const b = run();
|
|
606
|
+
assert.match(a, /2026-01-01 · 3 ships/);
|
|
607
|
+
assert.match(b, /2026-01-01 · 3 ships/);
|
|
608
|
+
// and the cell index is identical in both — no column/row drift.
|
|
609
|
+
assert.equal(a, b, 'the grid is byte-identical across extreme timezones');
|
|
610
|
+
} finally { process.env.TZ = saved; }
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test('CAL: empty history draws nothing, so the caller shows its own empty state', () => {
|
|
614
|
+
assert.equal(kit.calendarGridHtml([], {}), '');
|
|
615
|
+
assert.equal(kit.calendarGridHtml(null, {}), '');
|
|
616
|
+
assert.equal(kit.calendarGridHtml([{ local_date: '2026-09-03', count: 0 }], {}), '',
|
|
617
|
+
'a zero-count day is not a ship — nothing to draw');
|
|
618
|
+
assert.equal(kit.calendarGridHtml([{ local_date: 'not-a-date', count: 2 }], {}), '',
|
|
619
|
+
'an unparseable date is dropped, not floored to the epoch');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('CAL: the grid gets its own horizontal strip — it never widens the document', () => {
|
|
623
|
+
const html = kit.calendarGridHtml([{ local_date: '2026-09-03', count: 1 }], { today: '2026-09-03' });
|
|
624
|
+
assert.match(html, /kit-cal__scroll/, 'the wide grid scrolls inside its own strip');
|
|
625
|
+
});
|
|
@@ -122,7 +122,10 @@ test('profile-charts.js loads before profile.js, and profile.js hands it the pag
|
|
|
122
122
|
const a = html.indexOf('/builders/profile-charts.js');
|
|
123
123
|
const b = html.indexOf('/builders/profile.js');
|
|
124
124
|
assert.ok(a > 0 && a < b, 'the charts script is in the page, ahead of profile.js');
|
|
125
|
-
|
|
125
|
+
// task 1003572 threads the viewer's own streak (for the contribution calendar) as a
|
|
126
|
+
// fifth field; the assertion checks the stable prefix rather than the exact tail.
|
|
127
|
+
assert.match(profileJs, /window\.OTBProfileCharts\.render\(\{ api, API, builderId: builder\.id, selfId, signedIn: selfId != null/);
|
|
128
|
+
assert.match(profileJs, /selfStreak: \(selfMe && selfMe\.streak\) \|\| null/, 'the own-streak is handed to the charts for the calendar');
|
|
126
129
|
assert.match(html, /<section class="scroll" id="charts-scroll"[^>]*>[\s\S]*?<div class="pcharts" id="charts-grid">/, 'the charts section is a .pcharts grid');
|
|
127
130
|
});
|
|
128
131
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// tests/me_ship_days_bound.mjs — the year bound on /me's ship_days (task 1003572,
|
|
2
|
+
// criterion legibility-series-are-drawn).
|
|
3
|
+
//
|
|
4
|
+
// The row array computeStreak produces is unbounded, and /me is the hall's busiest
|
|
5
|
+
// boot read, so shipping the whole history is the exact over-fetch a sibling criterion
|
|
6
|
+
// (legibility-sort-and-staleness) names. This pins the two properties that make the
|
|
7
|
+
// field safe to add: it is BOUNDED to about a year, and the cutoff is TIMEZONE-SAFE —
|
|
8
|
+
// computed on the YYYY-MM-DD-as-UTC convention, never the server's local clock.
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { boundShipDaysToYear } = require('../src/bongos/routes/me.js');
|
|
14
|
+
|
|
15
|
+
// A pure ordinal helper for building fixtures, matching the module's own convention.
|
|
16
|
+
const ord = (y, m, d) => Math.floor(Date.UTC(y, m - 1, d) / 86400000);
|
|
17
|
+
const ymd = (o) => { const dt = new Date(o * 86400000); return `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, '0')}-${String(dt.getUTCDate()).padStart(2, '0')}`; };
|
|
18
|
+
|
|
19
|
+
test('keeps roughly a year and drops what is older', () => {
|
|
20
|
+
const today = '2026-09-03';
|
|
21
|
+
const todayOrd = ord(2026, 9, 3);
|
|
22
|
+
const rows = [];
|
|
23
|
+
// one row every 10 days going back ~2 years
|
|
24
|
+
for (let o = todayOrd; o > todayOrd - 730; o -= 10) rows.unshift({ local_date: ymd(o), count: 1 });
|
|
25
|
+
const out = boundShipDaysToYear(rows, today);
|
|
26
|
+
assert.ok(out.length > 0);
|
|
27
|
+
// nothing older than the 371-day window survives
|
|
28
|
+
const cutoff = todayOrd - 370;
|
|
29
|
+
for (const r of out) {
|
|
30
|
+
const o = ord(...r.local_date.split('-').map(Number));
|
|
31
|
+
assert.ok(o >= cutoff && o <= todayOrd, `${r.local_date} is inside the year window`);
|
|
32
|
+
}
|
|
33
|
+
// and it is strictly smaller than the full 2-year input
|
|
34
|
+
assert.ok(out.length < rows.length, 'the old rows were dropped');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('a future-dated row (clock skew) is not kept', () => {
|
|
38
|
+
const out = boundShipDaysToYear([
|
|
39
|
+
{ local_date: '2026-09-03', count: 2 },
|
|
40
|
+
{ local_date: '2027-01-01', count: 1 }, // after today
|
|
41
|
+
], '2026-09-03');
|
|
42
|
+
assert.deepEqual(out.map((r) => r.local_date), ['2026-09-03']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('empty in, empty out — and a missing today anchor still bounds the array', () => {
|
|
46
|
+
assert.deepEqual(boundShipDaysToYear([], '2026-09-03'), []);
|
|
47
|
+
assert.deepEqual(boundShipDaysToYear(null, '2026-09-03'), []);
|
|
48
|
+
// No usable today → still bounded (last 371 rows), never the whole history.
|
|
49
|
+
const rows = Array.from({ length: 1000 }, (_, i) => ({ local_date: ymd(ord(2020, 1, 1) + i), count: 1 }));
|
|
50
|
+
const out = boundShipDaysToYear(rows, 'not-a-date');
|
|
51
|
+
assert.equal(out.length, 371, 'falls back to a bounded tail, not all 1000');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('timezone-safe: the same rows bound to the same set under any TZ', () => {
|
|
55
|
+
// The point is invariance, not a specific boundary: `new Date("2025-01-01")` would be
|
|
56
|
+
// UTC-midnight and shift a day west of Greenwich, so a naive cutoff would keep a
|
|
57
|
+
// different set under UTC+14 vs UTC-11. Date.UTC on the parts does not.
|
|
58
|
+
const rows = [
|
|
59
|
+
{ local_date: '2024-01-01', count: 1 }, // ~20 months old → always dropped
|
|
60
|
+
{ local_date: '2025-06-15', count: 2 }, // ~15 months? within a year of 2026-09-03 → kept
|
|
61
|
+
{ local_date: '2026-09-03', count: 1 }, // today → kept
|
|
62
|
+
];
|
|
63
|
+
const saved = process.env.TZ;
|
|
64
|
+
const results = [];
|
|
65
|
+
try {
|
|
66
|
+
for (const tz of ['UTC', 'Pacific/Kiritimati', 'Pacific/Pago_Pago']) {
|
|
67
|
+
process.env.TZ = tz;
|
|
68
|
+
results.push(boundShipDaysToYear(rows, '2026-09-03').map((r) => r.local_date).join(','));
|
|
69
|
+
}
|
|
70
|
+
} finally { process.env.TZ = saved; }
|
|
71
|
+
assert.equal(results[0], results[1], 'UTC vs UTC+14 keep the same set');
|
|
72
|
+
assert.equal(results[1], results[2], 'UTC+14 vs UTC-11 keep the same set');
|
|
73
|
+
// and 2024 is out while today is in, whatever the TZ
|
|
74
|
+
assert.match(results[0], /2026-09-03/);
|
|
75
|
+
assert.doesNotMatch(results[0], /2024-01-01/);
|
|
76
|
+
});
|