From 789bd686e172aefe5111a7e21fb920a37a9f40cc Mon Sep 17 00:00:00 2001 From: Dhairya Date: Thu, 24 Sep 2026 21:49:47 +0530 Subject: [PATCH 1/2] feat: auto-close lab at 2 AM IST and notify general --- src-ts/openclose.ts | 28 ++++++++++++++++++++++++- test/cutover-regressions.js | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src-ts/openclose.ts b/src-ts/openclose.ts index 1279930..97c7979 100644 --- a/src-ts/openclose.ts +++ b/src-ts/openclose.ts @@ -6,9 +6,35 @@ // 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: "general" }, "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..4c61099 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 #general", 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, "general"); + 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 === "general").length, 2); +}); + test("persisted environment is restored without logging secret values", t => { preserveEnvironment(t, ["CUTOVER_BOOT_VALUE", "SLACK_BOT_TOKEN"]); const brain = new Brain(); From f07b58edf11d645b9445b2653a95370e126e34e4 Mon Sep 17 00:00:00 2001 From: Dhairya Date: Fri, 25 Sep 2026 09:21:19 +0530 Subject: [PATCH 2/2] fix: send lab auto-close notice to bottesting --- src-ts/openclose.ts | 12 ++++++++++-- test/cutover-regressions.js | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src-ts/openclose.ts b/src-ts/openclose.ts index 97c7979..cafeb24 100644 --- a/src-ts/openclose.ts +++ b/src-ts/openclose.ts @@ -29,11 +29,19 @@ export = (robot: Robot, now: () => Date = () => new Date()): void => { 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; + if ( + parts.hour !== "02" || + parts.minute !== "00" || + date === lastAutoCloseDate + ) + return; lastAutoCloseDate = date; status = "closed"; - robot.send({ room: "general" }, "Lab is now closed (auto-updated at 2:00 AM IST)."); + robot.send( + { room: "bottesting" }, + "Lab is now closed (auto-updated at 2:00 AM IST).", + ); }); robot.hear(/lab is (open|closed|close)/i, (msg) => { diff --git a/test/cutover-regressions.js b/test/cutover-regressions.js index 4c61099..ca769ef 100644 --- a/test/cutover-regressions.js +++ b/test/cutover-regressions.js @@ -91,7 +91,7 @@ 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 #general", async t => { +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, "* * * * *"); @@ -112,7 +112,7 @@ test("lab status auto-closes once daily at 2 AM Kolkata time in #general", async time = new Date("2026-09-23T20:30:00.000Z"); // 2:00 AM IST tick(); await bot.flush(); - assert.equal(sent.at(-1).envelope.room, "general"); + 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"); @@ -130,7 +130,7 @@ test("lab status auto-closes once daily at 2 AM Kolkata time in #general", async 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 === "general").length, 2); + assert.equal(sent.filter(item => item.envelope.room === "bottesting").length, 2); }); test("persisted environment is restored without logging secret values", t => {