From 255cfa699251cc1e16da09d75256b45e9e19a98c Mon Sep 17 00:00:00 2001 From: Tiberiu Socaci Date: Fri, 6 Nov 2026 21:34:53 +0200 Subject: [PATCH] Fix agent directory refresh time after spring DST gap Signed-off-by: Tiberiu Socaci --- CHANGELOG.md | 3 +++ FEATURES.md | 4 ++- TEST-PLAN.md | 6 +++++ src/gateway/agent-directory-refresh.js | 6 ++++- test/agent-directory-refresh.test.js | 35 ++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e029d65..f7ca434a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ release gates. Stable promotion and publication require approval of the exact te ### Beta development notes +- Restore the agent directory refresh to 03:00 local time after a spring-forward clock change + instead of retaining the transition day’s normalized 04:00 hour. + - Clarify that the network tool's policy is advisory in `/sudo` direct-host threads, with no OS-level egress cutoff. Ordinary proxy-mode container enforcement is unchanged. diff --git a/FEATURES.md b/FEATURES.md index 8786d830..a19a5eb2 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -428,7 +428,9 @@ Slack’s native running/Stop label shows the resolved model and explicitly conf recommends a channel without sending messages, delegating or transferring files. Organization skills can supply company terminology; no company directory is hardcoded in the product skill. - Built-in reconciliation refreshes channel metadata at startup and nightly at 03:00 in the host - local timezone without an engine turn or user schedule. Metadata caches narrow keyword searches; + local timezone without an engine turn or user schedule. A missing spring-forward 03:00 runs at + the first valid local time and the next day returns to 03:00; autumn runs once per local day. + Metadata caches narrow keyword searches; returned recommendations still verify live metadata/membership and fresh gateway access. The daemon registers its existing Slack lifecycle manager in the live connector registry; disconnects fail closed and reconnects resolve the current client without retaining old readers. diff --git a/TEST-PLAN.md b/TEST-PLAN.md index 15a5b28d..02cca922 100644 --- a/TEST-PLAN.md +++ b/TEST-PLAN.md @@ -862,6 +862,12 @@ inbox). Record the address and the servers; never the password. ## Channel agent directory (2026-10-21) +- [x] DST rearm regression (2026-11-06): `test/agent-directory-refresh.test.js` runs in an + explicit `Europe/Bucharest` subprocess. Ordinary dates retain 03:00, the missing spring 03:00 + normalizes to 04:00, the following day returns to 03:00 without drift, and the repeated autumn + hour produces only one refresh per local day. Disposable lifecycle acceptance also observes + the actual timer rearm across the spring gap; this does not claim a deployed overnight run. + - [x] Production boot integration: `test/agent-directory-slack-boot.test.js` uses the real Slack manager and `createPlatformTransports({ slack })` with a scratch store and simulated provider. Search uses its default verifier (no directory/connector override), returns the current channel diff --git a/src/gateway/agent-directory-refresh.js b/src/gateway/agent-directory-refresh.js index 674efe78..2549c13d 100644 --- a/src/gateway/agent-directory-refresh.js +++ b/src/gateway/agent-directory-refresh.js @@ -122,7 +122,11 @@ export async function verifyDirectoryChannel({ entry, createdBy, channelId }, de export function nextAgentDirectoryRefreshDelay(at = Date.now()) { const next = new Date(at); next.setHours(3, 0, 0, 0); - if (next.getTime() <= at) next.setDate(next.getDate() + 1); + if (next.getTime() <= at) { + next.setDate(next.getDate() + 1); + // A missing 03:00 can normalize to 04:00; restore the intended hour on the next date. + next.setHours(3, 0, 0, 0); + } return next.getTime() - at; } diff --git a/test/agent-directory-refresh.test.js b/test/agent-directory-refresh.test.js index 6578956c..26bd4a4e 100644 --- a/test/agent-directory-refresh.test.js +++ b/test/agent-directory-refresh.test.js @@ -1,5 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; import { ensureTestEnv } from "./helpers.js"; ensureTestEnv(); const { @@ -160,3 +161,37 @@ test("local-calendar 03:00 deadline is strictly future, including at the boundar const tomorrow = new Date(2026, 9, 22, 3).getTime(); assert.equal(nextAgentDirectoryRefreshDelay(boundary), tomorrow - boundary); }); + + +test("local 03:00 refresh returns to 03:00 after the spring DST gap without autumn drift", () => { + const moduleUrl = new URL("../src/gateway/agent-directory-refresh.js", import.meta.url).href; + const script = ` + const { nextAgentDirectoryRefreshDelay } = await import(${JSON.stringify(moduleUrl)}); + const inputs = [ + "2026-02-10T02:59:59+02:00", + "2026-03-28T03:00:00+02:00", + "2026-03-29T02:59:59+02:00", + "2026-03-29T04:00:00+03:00", + "2026-03-29T12:00:00+03:00", + "2026-10-24T03:00:00+03:00", + "2026-10-25T03:00:00+03:00", + ]; + console.log(JSON.stringify(inputs.map(input => { + const at = Date.parse(input); + return new Date(at + nextAgentDirectoryRefreshDelay(at)).toISOString(); + }))); + `; + const result = spawnSync(process.execPath, ["--input-type=module", "-e", script], { + env: { ...process.env, TZ: "Europe/Bucharest" }, encoding: "utf8", timeout: 10_000, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout.trim()), [ + "2026-02-10T01:00:00.000Z", + "2026-03-29T01:00:00.000Z", // Missing local 03:00 normalizes to the first valid 04:00. + "2026-03-29T01:00:00.000Z", + "2026-03-30T00:00:00.000Z", // The next ordinary day must return to local 03:00. + "2026-03-30T00:00:00.000Z", + "2026-10-25T00:00:00.000Z", + "2026-10-26T01:00:00.000Z", // One deadline per local day across the repeated hour. + ]); +});