fix: bound status-card active-hours calculation work

Reuse bounded timezone formatters and upcoming openings only within their exact searched closed ranges. Short-circuit empty windows while preserving the existing cold DST search and precise instant eligibility. Verify cost bounds, cache eviction, timezone transitions and twenty-card ordinary source delivery.

Co-Authored-By: Paperclip <paperclip@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-30 19:41:19 -05:00
1 parent f201195a92
commit 6469daec07
4 files changed
+225 -13

No files matched your search

@@ -141,3 +141,17 @@ deadline-reconstruction hint. Every card mutation requires a company ID; only
instance-level settings retain wildcard ingress. Deleting an already-missing
card emits no hint. The real two-company regression proves deletion does not
re-run unchanged reactive searches or wake either summarizer, then stays quiet.
Active-hours eligibility keeps the existing exact UTC-minute search across DST
changes. A bounded 32-entry timezone formatter cache removes repeated ICU
construction. A bounded 256-entry cache reuses the next opening for the exact
(timezone,start,end) policy only while the candidate instant stays within its
previously searched closed range. The precise instant is checked first, so
historical second-based offsets, the opening itself, and millisecond deadlines
keep their behavior. Clock rollback and policy changes cannot reuse the wrong
range. Empty windows return without formatter or search work. A cold nonempty
policy can still perform at most 2881 conversions including its first check; warm
closed-range evaluations perform one conversion, not another minute walk.
Both caches use bounded LRU retention and hold no company/card data. Tests prove
eviction, skipped/repeated hours, half-hour DST, date-line changes, rollback,
strict boundaries, and actual twenty-card source delivery followed by quiet.
@@ -272,6 +272,53 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
await quiet(); expect(errors).toEqual([]);
}, 30_000);
it("bounds active-hours conversion work across twenty cards and repeated ordinary company writes", async () => {
vi.setSystemTime(new Date("2026-11-02T18:00:00Z"));
const hoursPolicy = { ...policy(), activeHours: { start: "09:00", end: "17:00", timezone: "UTC" } };
const cards = [];
for (let i = 0; i < 20; i++) cards.push(await card("reactive", hoursPolicy));
await start(); await quiet();
const RealFormatter = Intl.DateTimeFormat;
const actualParts = RealFormatter.prototype.formatToParts;
let constructions = 0, conversions = 0;
const constructor = vi.spyOn(Intl, "DateTimeFormat").mockImplementation(function (...args: ConstructorParameters<typeof Intl.DateTimeFormat>) {
if (args[0] === "en-GB" && args[1]?.hourCycle === "h23") constructions++;
return new RealFormatter(...args);
} as unknown as typeof Intl.DateTimeFormat);
const parts = vi.spyOn(RealFormatter.prototype, "formatToParts").mockImplementation(function (this: Intl.DateTimeFormat, date) {
if (this.resolvedOptions().locale === "en-GB" && this.resolvedOptions().hourCycle === "h23") conversions++;
return actualParts.call(this, date);
});
const target = await issueService(db).create(companyId, { title: "Closed-hours blocker", status: "blocked", priority: "medium" });
await settled();
expect(constructions).toBeLessThanOrEqual(1);
expect(conversions).toBeLessThanOrEqual(1000);
const cold = { constructions, conversions };
constructions = 0; conversions = 0;
for (let i = 0; i < 10; i++) {
await issueService(db).addComment(target.id, `Closed-hours human update ${i}`, { userId: "board-user" });
await settled();
}
expect(constructions).toBe(0);
expect(conversions).toBeGreaterThanOrEqual(200);
expect(conversions).toBeLessThanOrEqual(800);
expect(wakeup).not.toHaveBeenCalled();
const opening = new Date("2026-11-03T09:00:00Z");
for (const created of cards) expect((await statusCardService(db).getById(created.id)))
.toMatchObject({ state: "paused_hours", pendingChangeCount: 1, nextEvalAt: opening });
process.stdout.write(JSON.stringify({ measurement: "status-card company active-hours workload", cards: 20,
sourceWrites: 11, cold, warm: { constructions, conversions } }) + "\n");
constructor.mockRestore(); parts.mockRestore();
await vi.advanceTimersByTimeAsync(opening.getTime() - Date.now()); await settled();
expect(wakeup).toHaveBeenCalledTimes(20);
for (const created of cards) {
const generated = (await statusCardService(db).getById(created.id))!;
expect(generated.generatingIssueId).toBeTruthy();
await writeback(created.id, generated.generatingIssueId!);
}
await quiet(); expect(errors).toEqual([]);
}, 60_000);
it("cancels archived or disabled future work and replaces an earlier interval deadline", async () => {
await start(); let created = await card("interval"); await settled();
const old = coordinator!.nextWakeAt();
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { statusCardRefreshPolicySchema } from "@paperclipai/shared";
import {
chooseStatusCardUpdateKind,
@@ -13,6 +13,7 @@ import {
} from "../services/status-card-update-engine.js";
describe("status card update engine", () => {
afterEach(() => vi.restoreAllMocks());
const defaultPolicy = statusCardRefreshPolicySchema.parse({ mode: "interval", intervalMinutes: 15 });
it("retains non-terminal and terminal status transitions plus membership changes", () => {
@@ -101,6 +102,127 @@ describe("status card update engine", () => {
expect(nextStatusCardPolicyEligibility({ policy, now, lastChangeAt: now, completedUpdateTimes: [], tokensToday: 0 })).toBeNull();
});
it("bounds empty-window formatter work before any timezone conversion", () => {
const policy = statusCardRefreshPolicySchema.parse({ mode: "reactive", debounceSeconds: 1,
activeHours: { start: "09:00", end: "09:00", timezone: "Pacific/Chatham" } });
const formatter = vi.spyOn(Intl, "DateTimeFormat").mockImplementation(function () {
throw new Error("Empty active hours must not construct a formatter");
});
const now = new Date("2026-07-23T14:00:00Z");
expect(isWithinStatusCardActiveHours(policy, now)).toBe(false);
expect(nextStatusCardPolicyEligibility({ policy, now, lastChangeAt: now, completedUpdateTimes: [], tokensToday: 0 })).toBeNull();
expect(formatter).not.toHaveBeenCalled();
});
it("bounds formatter construction and reuses the next opening across cards and source writes", () => {
const policy = statusCardRefreshPolicySchema.parse({ mode: "reactive", debounceSeconds: 1,
activeHours: { start: "09:00", end: "17:00", timezone: "America/St_Johns" } });
const RealFormatter = Intl.DateTimeFormat;
let constructions = 0;
vi.spyOn(Intl, "DateTimeFormat").mockImplementation(function (...args: ConstructorParameters<typeof Intl.DateTimeFormat>) {
if (++constructions > 1) throw new Error("Card evaluation repeatedly constructed timezone formatters");
return new RealFormatter(...args);
} as unknown as typeof Intl.DateTimeFormat);
const conversions = vi.spyOn(RealFormatter.prototype, "formatToParts");
const now = new Date("2026-07-23T22:00:00Z");
const input = { policy, now, lastChangeAt: new Date(now.getTime() - 60_000), completedUpdateTimes: [], tokensToday: 0 };
const started = performance.now();
expect(nextStatusCardPolicyEligibility(input)).toEqual(new Date("2026-07-24T11:30:00Z"));
const coldMs = performance.now() - started;
const coldConversions = conversions.mock.calls.length;
expect(coldConversions).toBeLessThanOrEqual(2881);
conversions.mockClear();
const warmStarted = performance.now();
for (let write = 0; write < 100; write++) for (let card = 0; card < 20; card++) {
expect(nextStatusCardPolicyEligibility({ ...input,
policy: { ...policy, activeHours: { ...policy.activeHours! } },
now: new Date(now.getTime() + write * 1000),
})).toEqual(new Date("2026-07-24T11:30:00Z"));
}
expect(constructions).toBeLessThanOrEqual(1);
expect(conversions.mock.calls.length).toBeLessThanOrEqual(2000);
process.stdout.write(JSON.stringify({ measurement: "status-card active-hours cost", coldMs, coldConversions, constructions,
warmMs: performance.now() - warmStarted, warmConversions: conversions.mock.calls.length, evaluations: 2000 }) + "\n");
});
it.each([
["America/New_York", "02:15", "03:30", "2026-03-08T06:50:00Z", "2026-03-08T07:00:00Z"],
["America/New_York", "02:15", "02:45", "2026-03-08T06:50:00Z", "2026-03-09T06:15:00Z"],
["America/New_York", "02:15", "02:45", "2026-03-07T07:45:00Z", "2026-03-09T06:15:00Z"],
["America/New_York", "01:15", "01:45", "2026-11-01T05:50:00Z", "2026-11-01T06:15:00Z"],
["America/New_York", "23:30", "01:30", "2026-11-01T05:40:00Z", "2026-11-01T06:00:00Z"],
["Australia/Lord_Howe", "02:15", "02:45", "2026-10-03T15:20:00Z", "2026-10-03T15:30:00Z"],
["Australia/Lord_Howe", "01:35", "01:45", "2026-04-04T14:50:00Z", "2026-04-04T15:05:00Z"],
["Asia/Kathmandu", "09:00", "17:00", "2026-07-23T12:00:00Z", "2026-07-24T03:15:00Z"],
["Pacific/Apia", "00:10", "00:20", "2011-12-30T09:50:00Z", "2011-12-30T10:10:00Z"],
])("preserves the first real opening in %s %s-%s after %s", (timezone, start, end, time, expected) => {
const policy = statusCardRefreshPolicySchema.parse({ mode: "reactive", debounceSeconds: 1, activeHours: { start, end, timezone } });
const now = new Date(time);
const input = { policy, now, lastChangeAt: new Date(now.getTime() - 60_000), completedUpdateTimes: [], tokensToday: 0 };
expect(nextStatusCardPolicyEligibility(input)).toEqual(new Date(expected));
// A later closed instant reuses the opening, while the exact opening itself
// remains eligible without rounding it into the next minute or day.
expect(nextStatusCardPolicyEligibility({ ...input, now: new Date(now.getTime() + 1000) })).toEqual(new Date(expected));
const opening = new Date(expected);
expect(nextStatusCardPolicyEligibility({ ...input, now: opening })).toEqual(opening);
expect(nextStatusCardPolicyEligibility({ ...input, now: new Date(opening.getTime() + 1) })).toEqual(new Date(opening.getTime() + 1));
});
it("invalidates an upcoming opening for clock rollback and changed policy values", () => {
const policy = statusCardRefreshPolicySchema.parse({ mode: "reactive", debounceSeconds: 1,
activeHours: { start: "09:00", end: "17:00", timezone: "UTC" } });
const calculate = (time: string, activeHours = policy.activeHours!) => {
const now = new Date(time);
return nextStatusCardPolicyEligibility({ policy: { ...policy, activeHours }, now,
lastChangeAt: new Date(now.getTime() - 60_000), completedUpdateTimes: [], tokensToday: 0 });
};
expect(calculate("2026-07-23T18:00:00Z")).toEqual(new Date("2026-07-24T09:00:00Z"));
expect(calculate("2026-07-23T08:00:00Z")).toEqual(new Date("2026-07-23T09:00:00Z"));
expect(calculate("2026-07-23T17:00:00Z")).toEqual(new Date("2026-07-24T09:00:00Z"));
expect(calculate("2026-07-23T18:00:00Z", { ...policy.activeHours!, start: "10:00" })).toEqual(new Date("2026-07-24T10:00:00Z"));
expect(calculate("2026-07-23T18:00:00Z", { ...policy.activeHours!, end: "19:00" })).toEqual(new Date("2026-07-23T18:00:00Z"));
expect(calculate("2026-07-23T18:00:00Z", { ...policy.activeHours!, timezone: "Asia/Tokyo" })).toEqual(new Date("2026-07-24T00:00:00Z"));
});
it("checks the precise instant before a cached minute opening for historical second offsets", () => {
const policy = statusCardRefreshPolicySchema.parse({ mode: "reactive", debounceSeconds: 1,
activeHours: { start: "09:00", end: "10:00", timezone: "Africa/Monrovia" } });
const now = new Date("1970-01-01T08:00:00Z");
const input = { policy, now, lastChangeAt: new Date(now.getTime() - 60_000), completedUpdateTimes: [], tokensToday: 0 };
expect(nextStatusCardPolicyEligibility(input)).toEqual(new Date("1970-01-01T09:45:00Z"));
const inside = new Date("1970-01-01T09:44:45Z");
expect(isWithinStatusCardActiveHours(policy, inside)).toBe(true);
expect(nextStatusCardPolicyEligibility({ ...input, now: inside })).toEqual(inside);
});
it("bounds formatter and upcoming-opening cache retention", () => {
const now = new Date("2026-07-25T00:00:00Z");
const timezonePolicy = (timezone: string) => statusCardRefreshPolicySchema.parse({ mode: "reactive", debounceSeconds: 1,
activeHours: { start: "09:00", end: "17:00", timezone } });
const zones = Intl.supportedValuesOf("timeZone").slice(0, 34);
const first = timezonePolicy(zones[0]!);
isWithinStatusCardActiveHours(first, now);
for (const zone of zones.slice(1)) isWithinStatusCardActiveHours(timezonePolicy(zone), now);
const RealFormatter = Intl.DateTimeFormat;
const constructor = vi.spyOn(Intl, "DateTimeFormat").mockImplementation(function (...args: ConstructorParameters<typeof Intl.DateTimeFormat>) {
return new RealFormatter(...args);
} as unknown as typeof Intl.DateTimeFormat);
isWithinStatusCardActiveHours(first, now);
expect(constructor).toHaveBeenCalledTimes(1);
constructor.mockRestore();
const time = (minute: number) => `${String(Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`;
const calculate = (minute: number) => {
const policy = statusCardRefreshPolicySchema.parse({ mode: "reactive", debounceSeconds: 1,
activeHours: { start: time(minute), end: time(minute + 1), timezone: "UTC" } });
return nextStatusCardPolicyEligibility({ policy, now, lastChangeAt: new Date(now.getTime() - 60_000), completedUpdateTimes: [], tokensToday: 0 });
};
calculate(1);
for (let minute = 2; minute <= 258; minute++) calculate(minute);
const conversions = vi.spyOn(Intl.DateTimeFormat.prototype, "formatToParts");
expect(calculate(1)).toEqual(new Date("2026-07-25T00:01:00Z"));
expect(conversions.mock.calls.length).toBeGreaterThan(1);
});
it("selects full rebuilds for bounded drift rules and incremental otherwise", () => {
const base = { hasDocument: true, changeCount: 2, queryVersion: 3, lastUpdateQueryVersion: 3, incrementalCount: 2, configurationChanged: false };
expect(chooseStatusCardUpdateKind(base)).toBe("incremental");
@@ -109,21 +109,36 @@ export function statusCardFingerprintHash(fingerprint: StatusCardFingerprint) {
return createHash("sha256").update(JSON.stringify(stable)).digest("hex");
}
// These caches contain only timezone/policy calculations, never card or company
// data. Bound both maps because timezone aliases and hour pairs are user input.
const activeHoursFormatters = new Map<string, Intl.DateTimeFormat>();
const upcomingActiveHours = new Map<string, { closedAt: number; opensAt: number }>();
function remember<K, V>(cache: Map<K, V>, key: K, value: V, limit: number): V {
cache.delete(key);
cache.set(key, value);
if (cache.size > limit) cache.delete(cache.keys().next().value!);
return value;
}
function activeHoursFormatter(timezone: string) {
const cached = activeHoursFormatters.get(timezone);
return remember(activeHoursFormatters, timezone, cached ?? new Intl.DateTimeFormat("en-GB", {
timeZone: timezone, hour: "2-digit", minute: "2-digit", hourCycle: "h23",
}), 32);
}
export function isWithinStatusCardActiveHours(policy: StatusCardRefreshPolicy, now: Date) {
if (!policy.activeHours) return true;
const parts = new Intl.DateTimeFormat("en-GB", {
timeZone: policy.activeHours.timezone,
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
}).formatToParts(now);
const hour = Number(parts.find((part) => part.type === "hour")?.value ?? 0);
const minute = Number(parts.find((part) => part.type === "minute")?.value ?? 0);
const current = hour * 60 + minute;
const [startHour, startMinute] = policy.activeHours.start.split(":").map(Number);
const [endHour, endMinute] = policy.activeHours.end.split(":").map(Number);
const start = startHour! * 60 + startMinute!;
const end = endHour! * 60 + endMinute!;
if (start === end) return false;
const parts = activeHoursFormatter(policy.activeHours.timezone).formatToParts(now);
const hour = Number(parts.find((part) => part.type === "hour")?.value ?? 0);
const minute = Number(parts.find((part) => part.type === "minute")?.value ?? 0);
const current = hour * 60 + minute;
return start <= end ? current >= start && current < end : current >= start || current < end;
}
@@ -181,6 +196,8 @@ export function nextStatusCardPolicyEligibility(input: {
tokensToday: number;
}): Date | null {
if (input.policy.mode === "manual") return null;
const hours = input.policy.activeHours;
if (hours && hours.start === hours.end) return null;
let at = input.now.getTime();
if (input.tokensToday >= (input.policy.dailyTokenCap ?? 100_000)) {
const day = new Date(input.now);
@@ -194,13 +211,25 @@ export function nextStatusCardPolicyEligibility(input: {
.filter(time => time > at - 3_600_000).sort((a, b) => a - b);
if (recent.length >= cap) at = Math.max(at, recent[recent.length - cap]! + 3_600_000 + 1);
}
if (isWithinStatusCardActiveHours(input.policy, new Date(at))) return new Date(at);
// Check the precise instant before reusing a minute-aligned opening. Some
// historical timezone offsets include seconds, so a window may be open now.
if (!hours || isWithinStatusCardActiveHours(input.policy, new Date(at))) return new Date(at);
const key = JSON.stringify([hours.timezone, hours.start, hours.end]);
const cached = upcomingActiveHours.get(key);
if (cached && at >= cached.closedAt && at < cached.opensAt) {
remember(upcomingActiveHours, key, cached, 256);
return new Date(cached.opensAt);
}
const closedAt = at;
// Active-hour precision is one local minute. Walking UTC minutes also honors
// skipped/repeated local times at daylight-saving transitions.
const end = at + 48 * 3_600_000;
for (at = Math.floor(at / 60_000) * 60_000 + 60_000; at <= end; at += 60_000) {
if (isWithinStatusCardActiveHours(input.policy, new Date(at))) return new Date(at);
if (isWithinStatusCardActiveHours(input.policy, new Date(at))) {
remember(upcomingActiveHours, key, { closedAt, opensAt: at }, 256);
return new Date(at);
}
}
// Equal start/end describes an empty window; retain changes, awaiting config.
// No opening inside the existing bounded search horizon. Retain changes.
return null;
}