Skip to content

Commit 326db58

Browse files
committed
fix(gateway): guard hook job timestamps
1 parent 3caf4fa commit 326db58

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

src/gateway/server.hooks.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,18 @@ type HookCronRunCall = {
9595
sessionKey?: string;
9696
job?: {
9797
agentId?: string;
98+
createdAtMs?: number;
9899
payload?: {
99100
externalContentSource?: string;
100101
model?: string;
101102
};
103+
schedule?: {
104+
kind?: string;
105+
at?: string;
106+
};
107+
state?: {
108+
nextRunAtMs?: number;
109+
};
102110
};
103111
};
104112

@@ -792,6 +800,32 @@ describe("gateway server hooks", () => {
792800
});
793801
});
794802

803+
test("dispatches agent hooks when the process clock is outside the Date range", async () => {
804+
testState.hooksConfig = { enabled: true, token: HOOK_TOKEN };
805+
806+
await withGatewayServer(async ({ port }) => {
807+
mockIsolatedRunOkOnce();
808+
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
809+
810+
try {
811+
const response = await postHook(port, "/hooks/agent", {
812+
message: "Bad clock",
813+
name: "Clock",
814+
});
815+
expect(response.status).toBe(200);
816+
await waitForSystemEvent();
817+
} finally {
818+
dateNowSpy.mockRestore();
819+
}
820+
821+
const call = cronRunCall();
822+
expect(call.job?.createdAtMs).toBe(0);
823+
expect(call.job?.schedule).toEqual({ kind: "at", at: "1970-01-01T00:00:00.000Z" });
824+
expect(call.job?.state?.nextRunAtMs).toBe(0);
825+
drainSystemEvents(resolveMainKey());
826+
});
827+
});
828+
795829
test("enforces hooks.allowedAgentIds for effective agent routing", async () => {
796830
testState.hooksConfig = {
797831
enabled: true,

src/gateway/server/hooks.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import type { CronJob } from "../../cron/types.js";
1313
import { requestHeartbeat } from "../../infra/heartbeat-wake.js";
1414
import { enqueueSystemEvent } from "../../infra/system-events.js";
1515
import type { createSubsystemLogger } from "../../logging/subsystem.js";
16+
import {
17+
resolveDateTimestampMs,
18+
resolveTimestampMsToIsoString,
19+
} from "../../shared/number-coercion.js";
1620
import { normalizeOptionalString } from "../../shared/string-coerce.js";
1721
import { type HookAgentDispatchPayload, type HooksConfigResolved } from "../hooks.js";
1822
import { createHooksRequestHandler, type HookClientIpConfig } from "./hooks-request-handler.js";
@@ -105,7 +109,7 @@ export function createGatewayHooksRequestHandler(params: {
105109
const safeName = sanitizeInboundSystemTags(value.name);
106110
const jobId = randomUUID();
107111
const runId = randomUUID();
108-
const now = Date.now();
112+
const nowMs = resolveDateTimestampMs(Date.now());
109113
const delivery = value.deliver
110114
? {
111115
mode: "announce" as const,
@@ -118,9 +122,9 @@ export function createGatewayHooksRequestHandler(params: {
118122
agentId: value.agentId,
119123
name: safeName,
120124
enabled: true,
121-
createdAtMs: now,
122-
updatedAtMs: now,
123-
schedule: { kind: "at", at: new Date(now).toISOString() },
125+
createdAtMs: nowMs,
126+
updatedAtMs: nowMs,
127+
schedule: { kind: "at", at: resolveTimestampMsToIsoString(nowMs) },
124128
sessionTarget: "isolated",
125129
wakeMode: value.wakeMode,
126130
payload: {
@@ -133,7 +137,7 @@ export function createGatewayHooksRequestHandler(params: {
133137
externalContentSource: value.externalContentSource,
134138
},
135139
delivery,
136-
state: { nextRunAtMs: now },
140+
state: { nextRunAtMs: nowMs },
137141
};
138142

139143
let hookEventSessionKey: string | undefined;

0 commit comments

Comments
 (0)