diff --git a/src-ts/openclose.ts b/src-ts/openclose.ts index 1279930..cafeb24 100644 --- a/src-ts/openclose.ts +++ b/src-ts/openclose.ts @@ -6,9 +6,43 @@ // bot lab is open/close import type { Robot } from "./runtime/types"; +import * as cron from "node-cron"; -export = (robot: Robot): void => { +const istTime = new Intl.DateTimeFormat("en-GB", { + timeZone: "Asia/Kolkata", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hourCycle: "h23", +}); + +export = (robot: Robot, now: () => Date = () => new Date()): void => { let status = ""; + let lastAutoCloseDate = ""; + + // node-cron 1.x uses the host timezone, so check the Kolkata wall clock + // explicitly instead of relying on the deployment's TZ setting. + cron.schedule("* * * * *", () => { + const parts = Object.fromEntries( + istTime.formatToParts(now()).map(({ type, value }) => [type, value]), + ); + const date = `${parts.year}-${parts.month}-${parts.day}`; + if ( + parts.hour !== "02" || + parts.minute !== "00" || + date === lastAutoCloseDate + ) + return; + + lastAutoCloseDate = date; + status = "closed"; + robot.send( + { room: "bottesting" }, + "Lab is now closed (auto-updated at 2:00 AM IST).", + ); + }); robot.hear(/lab is (open|closed|close)/i, (msg) => { status = msg.match[1]; diff --git a/test/cutover-regressions.js b/test/cutover-regressions.js index 8229dbd..ca769ef 100644 --- a/test/cutover-regressions.js +++ b/test/cutover-regressions.js @@ -91,6 +91,48 @@ test("all production scripts register offline and retain help commands", async t assert(sent.some(item => /^https?:\/\//.test(item.messages[0]))); }); +test("lab status auto-closes once daily at 2 AM Kolkata time in #bottesting", async t => { + let tick; + t.mock.method(cron, "schedule", (expression, callback) => { + assert.equal(expression, "* * * * *"); + tick = callback; + return { stop() {} }; + }); + const { bot, sent } = botFixture(); + let time = new Date("2026-09-23T20:29:00.000Z"); // 1:59 AM IST + require("../scripts/openclose")(bot, () => time); + + await command(bot, "bot lab is open"); + tick(); + await bot.flush(); + assert.equal(sent.length, 1); + await command(bot, "bot is lab open"); + assert.equal(sent.at(-1).messages[0], "lab is open"); + + time = new Date("2026-09-23T20:30:00.000Z"); // 2:00 AM IST + tick(); + await bot.flush(); + assert.equal(sent.at(-1).envelope.room, "bottesting"); + assert.equal(sent.at(-1).messages[0], "Lab is now closed (auto-updated at 2:00 AM IST)."); + const count = sent.length; + time = new Date("2026-09-23T20:30:30.000Z"); + tick(); + await bot.flush(); + assert.equal(sent.length, count); + await command(bot, "bot is lab open"); + assert.equal(sent.at(-1).messages[0], "lab is closed"); + + await command(bot, "bot lab is open"); + await command(bot, "bot is lab open"); + assert.equal(sent.at(-1).messages[0], "lab is open"); + time = new Date("2026-09-24T20:30:00.000Z"); + tick(); + await bot.flush(); + await command(bot, "bot is lab open"); + assert.equal(sent.at(-1).messages[0], "lab is closed"); + assert.equal(sent.filter(item => item.envelope.room === "bottesting").length, 2); +}); + test("persisted environment is restored without logging secret values", t => { preserveEnvironment(t, ["CUTOVER_BOOT_VALUE", "SLACK_BOT_TOKEN"]); const brain = new Brain();