项目文件夹

文件
2026-07-13 13:39:12 +08:00

230 行
9.8 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import test from "node:test";
import assert from "node:assert/strict";
import {
rotationIndex,
shiftFor,
shiftsInRange,
formatInTimezone,
utcForTimezone,
} from "../../scripts/sre/oncall-rotation.mjs";
// ─── Helpers ────────────────────────────────────────────────────────────────
const PST = "America/Los_Angeles";
const HOUR = 3600_000;
const DAY = 24 * HOUR;
const WEEK = 7 * DAY;
const ROTATION = {
timezone: PST,
shiftHours: 168, // one week
startsAt: "2026-06-01T09:00:00-07:00", // Monday 09:00 PDT
members: ["alice", "bob", "carol", "dave"],
};
// ─── 1. formatInTimezone ────────────────────────────────────────────────────
test("formatInTimezone: formats a UTC instant in PST", () => {
// 2026-06-25T16:00:00Z = 2026-06-25T09:00:00-07:00 (PDT in June)
const out = formatInTimezone(new Date("2026-06-25T16:00:00Z"), PST);
assert.equal(out, "2026-06-25T09:00:00-07:00");
});
test("formatInTimezone: PDT in June, PST in January", () => {
// June → UTC-7 (PDT)
const june = formatInTimezone(new Date("2026-06-15T12:00:00Z"), PST);
assert.match(june, /-07:00$/);
// January → UTC-8 (PST)
const jan = formatInTimezone(new Date("2026-01-15T12:00:00Z"), PST);
assert.match(jan, /-08:00$/);
});
// ─── 2. utcForTimezone ──────────────────────────────────────────────────────
test("utcForTimezone: PDT noon is 19:00Z", () => {
const out = utcForTimezone(new Date("2026-06-15T12:00:00-07:00"), PST);
assert.equal(new Date(out).toISOString(), "2026-06-15T19:00:00.000Z");
});
test("utcForTimezone: PST noon is 20:00Z", () => {
const out = utcForTimezone(new Date("2026-01-15T12:00:00-08:00"), PST);
assert.equal(new Date(out).toISOString(), "2026-01-15T20:00:00.000Z");
});
// ─── 3. rotationIndex ───────────────────────────────────────────────────────
test("rotationIndex: starts at 0 for the rotation start instant", () => {
const startMs = Date.parse("2026-06-01T16:00:00Z"); // 09:00 PDT
assert.equal(rotationIndex(startMs, ROTATION), 0);
});
test("rotationIndex: advances to 1 exactly one shift later", () => {
const t = Date.parse("2026-06-08T16:00:00Z"); // one week later, 09:00 PDT
assert.equal(rotationIndex(t, ROTATION), 1);
});
test("rotationIndex: mid-week is still shift 0", () => {
const t = Date.parse("2026-06-03T16:00:00Z"); // Wednesday
assert.equal(rotationIndex(t, ROTATION), 0);
});
test("rotationIndex: wraps around after `members.length` shifts", () => {
// 4 members × 1 week each = 4 weeks later = back to alice (index 0).
const t = Date.parse("2026-06-01T16:00:00Z") + 4 * WEEK;
assert.equal(rotationIndex(t, ROTATION), 0);
});
test("rotationIndex: second cycle hits bob at week 5", () => {
const t = Date.parse("2026-06-01T16:00:00Z") + 5 * WEEK;
assert.equal(rotationIndex(t, ROTATION), 1);
});
// ─── 4. shiftFor ────────────────────────────────────────────────────────────
test("shiftFor: at the very start of the rotation", () => {
const startMs = Date.parse("2026-06-01T16:00:00Z");
const shift = shiftFor(startMs, ROTATION);
assert.equal(shift.member, "alice");
assert.equal(shift.shiftNumber, 0);
assert.equal(shift.startsAt, "2026-06-01T16:00:00.000Z");
});
test("shiftFor: at the start of week 2 (boundary)", () => {
const t = Date.parse("2026-06-08T16:00:00Z");
const shift = shiftFor(t, ROTATION);
assert.equal(shift.member, "bob");
assert.equal(shift.shiftNumber, 1);
assert.equal(shift.startsAt, "2026-06-08T16:00:00.000Z");
});
test("shiftFor: 1ms before the boundary still belongs to current shift", () => {
const t = Date.parse("2026-06-08T15:59:59.999Z");
const shift = shiftFor(t, ROTATION);
assert.equal(shift.member, "alice");
});
test("shiftFor: DST transition stays in the same shift", () => {
// DST in US jumped forward on 2026-03-08. Check that an instant
// straddling the spring-forward boundary still resolves to a single
// shift (no off-by-one).
const dstRotation = {
timezone: PST,
shiftHours: 168,
startsAt: "2026-03-01T09:00:00-08:00", // PST
members: ["alice", "bob"],
};
// 2026-03-08T02:30 PST does not exist (skipped by DST). The instant
// 2026-03-08T10:30:00Z = 03:30 PDT (which exists).
const afterDst = Date.parse("2026-03-08T10:30:00Z");
const beforeDst = Date.parse("2026-03-08T08:30:00Z"); // 00:30 PST
assert.equal(shiftFor(afterDst, dstRotation).member, shiftFor(beforeDst, dstRotation).member);
});
// ─── 5. shiftsInRange ───────────────────────────────────────────────────────
test("shiftsInRange: single shift fully inside the range", () => {
const shifts = shiftsInRange("2026-06-02T00:00:00Z", "2026-06-03T00:00:00Z", ROTATION);
assert.equal(shifts.length, 1);
assert.equal(shifts[0].member, "alice");
});
test("shiftsInRange: range spanning two shifts returns both", () => {
const shifts = shiftsInRange("2026-06-02T00:00:00Z", "2026-06-15T00:00:00Z", ROTATION);
assert.equal(shifts.length, 2);
assert.equal(shifts[0].member, "alice");
assert.equal(shifts[1].member, "bob");
});
test("shiftsInRange: range spanning one full cycle returns all 4 members", () => {
const shifts = shiftsInRange("2026-06-01T00:00:00Z", "2026-07-01T00:00:00Z", ROTATION);
assert.equal(shifts.length, 4);
assert.deepEqual(
shifts.map((s) => s.member),
["alice", "bob", "carol", "dave"],
);
});
test("shiftsInRange: empty range returns empty array", () => {
// Range entirely before the rotation starts.
const shifts = shiftsInRange("2026-05-01T00:00:00Z", "2026-05-02T00:00:00Z", ROTATION);
assert.equal(shifts.length, 0);
});
test("shiftsInRange: reversed range throws", () => {
assert.throws(
() => shiftsInRange("2026-06-15T00:00:00Z", "2026-06-01T00:00:00Z", ROTATION),
/empty or reversed/,
);
});
test("shiftsInRange: invalid datetime throws", () => {
assert.throws(
() => shiftsInRange("not-a-date", "2026-06-15T00:00:00Z", ROTATION),
/invalid date/,
);
});
// ─── 6. Cross-week + cross-DST handoff ─────────────────────────────────────
test("cross-week: handoff happens at the same wall-clock time each Monday", () => {
// Check that week 1 ends and week 2 begins at 09:00 PDT for both shifts.
const shifts = shiftsInRange("2026-06-07T00:00:00Z", "2026-06-15T00:00:00Z", ROTATION);
assert.equal(shifts.length, 2);
// Alice's shift ends at 2026-06-08T16:00:00Z.
assert.equal(shifts[0].endsAt, "2026-06-08T16:00:00.000Z");
// Bob's shift starts at 2026-06-08T16:00:00Z.
assert.equal(shifts[1].startsAt, "2026-06-08T16:00:00.000Z");
});
test("cross-DST: week spans the spring-forward boundary cleanly", () => {
// 2026-03-01 starts PST (UTC-8); 2026-03-08 jumps to PDT (UTC-7).
const dstRotation = {
timezone: PST,
shiftHours: 168,
startsAt: "2026-03-01T09:00:00-08:00",
members: ["alice", "bob"],
};
const shifts = shiftsInRange("2026-03-01T00:00:00Z", "2026-03-15T00:00:00Z", dstRotation);
assert.equal(shifts.length, 2);
// Both shifts should be 168 hours in duration, but their UTC start/end
// times shift by 1 hour because of DST.
assert.equal(shifts[0].member, "alice");
assert.equal(shifts[1].member, "bob");
// The UTC duration of each shift is still 168 hours (the UTC offset
// shifted, but the duration in real time is invariant).
const dur0 = Date.parse(shifts[0].endsAt) - Date.parse(shifts[0].startsAt);
assert.equal(dur0, 168 * HOUR);
});
// ─── 7. Negative / pre-rotation instants ───────────────────────────────────
test("rotationIndex: pre-rotation instant uses correct wrap-around", () => {
// A shift before `startsAt` should land on the member who WOULD have
// been on call at that earlier time. We walk backwards from startsAt.
const oneWeekBefore = Date.parse("2026-06-01T16:00:00Z") - WEEK;
// One shift earlier = last member (dave).
assert.equal(rotationIndex(oneWeekBefore, ROTATION), 3);
});
test("shiftFor: pre-rotation instant gives the expected member", () => {
const oneWeekBefore = Date.parse("2026-06-01T16:00:00Z") - WEEK;
const shift = shiftFor(oneWeekBefore, ROTATION);
assert.equal(shift.member, "dave");
assert.equal(shift.shiftNumber, -1);
});
// ─── 8. Different shift lengths ─────────────────────────────────────────────
test("rotationIndex: 24h shifts cycle faster than weekly shifts", () => {
const dailyRotation = { ...ROTATION, shiftHours: 24 };
const t = Date.parse("2026-06-03T16:00:00Z"); // 2 days after start
assert.equal(rotationIndex(t, dailyRotation), 2);
});
test("rotationIndex: 12h shifts cycle every half-day", () => {
const halfDayRotation = { ...ROTATION, shiftHours: 12 };
const t = Date.parse("2026-06-01T22:00:00Z"); // 6 hours after start
assert.equal(rotationIndex(t, halfDayRotation), 0);
const t2 = Date.parse("2026-06-02T04:00:00Z"); // 12 hours after start
assert.equal(rotationIndex(t2, halfDayRotation), 1);
});