From e14f34a498d9beb230aa0028db917f7744b35f8f Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:09:37 -0700 Subject: [PATCH 1/2] refactor(rebrand): rename brand literals in public API/webhook payloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the old brand name where it was baked into unauthenticated, publicly consumable data rather than code identifiers: the README badge SVG/JSON label, the /health and MCP-compatibility service descriptors, and the outbound Discord/Slack/PagerDuty notification payloads (the Discord username + embed footer text, the audit-ledger actor field across all three notification services, and the PagerDuty event source field). Also consolidates the badge.json 404 fallback in routes.ts to import badge.ts's LABEL constant instead of duplicating the literal, and fixes the resulting drift in the OpenAPI Health/McpCompatibility zod schemas plus the generated openapi.json, which otherwise would have kept asserting the old service literal against the renamed runtime response. Leaves gittensor (the permanent SN74 subnet brand), the repo's own name, domain/hostname literals, npm package identifiers, and check-run/PR- comment branding untouched — each is tracked by its own separate rebrand issue. Closes #5336 --- apps/gittensory-ui/public/openapi.json | 4 ++-- src/api/badge.ts | 2 +- src/api/routes.ts | 6 +++--- src/openapi/schemas.ts | 4 ++-- src/services/mcp-compatibility.ts | 4 ++-- src/services/notify-discord.ts | 22 +++++++++++----------- src/services/notify-pagerduty.ts | 8 ++++---- src/services/review-recap.ts | 16 ++++++++-------- test/integration/api.test.ts | 8 ++++---- test/unit/badge.test.ts | 4 ++-- test/unit/notify-pagerduty.test.ts | 4 ++-- test/unit/support/mcp-cli-harness.ts | 4 ++-- test/workers/worker-runtime.test.ts | 2 +- 13 files changed, 44 insertions(+), 44 deletions(-) diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index 3c8a50fff..45e5bf4be 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -19,7 +19,7 @@ "service": { "type": "string", "enum": [ - "gittensory-api" + "loopover-api" ] }, "time": { @@ -52,7 +52,7 @@ "service": { "type": "string", "enum": [ - "gittensory-api" + "loopover-api" ] }, "apiVersion": { diff --git a/src/api/badge.ts b/src/api/badge.ts index cd07bf1b1..9257b4ff7 100644 --- a/src/api/badge.ts +++ b/src/api/badge.ts @@ -5,7 +5,7 @@ import type { PublicRepoQuality, QueueHealthLevel } from "../services/public-rep // before it reaches the SVG so the unauthenticated, embeddable surface cannot be turned into an injection // vector even if upstream values ever change shape. -const LABEL = "gittensory"; +export const LABEL = "loopover"; const QUEUE_COLORS: Record = { low: "#3fb950", diff --git a/src/api/routes.ts b/src/api/routes.ts index 6b299af0c..0e9f33fb2 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -197,7 +197,7 @@ import { buildSelfDogfoodRegistrationPack, resolveSelfDogfoodRepoFullName } from import { buildSubnetInterfaceDescriptor } from "../services/subnet-interface"; import { buildPublicRepoQuality, type PublicRepoQuality } from "../services/public-repo-quality"; import { loadPublicQualityMetrics } from "../services/public-quality-metrics"; -import { buildShieldsBadge, renderBadgeSvg, renderUnavailableBadgeSvg } from "./badge"; +import { buildShieldsBadge, LABEL as PUBLIC_BADGE_LABEL, renderBadgeSvg, renderUnavailableBadgeSvg } from "./badge"; import { buildWeeklyValueReport, formatWeeklyValueReportMarkdown, @@ -936,7 +936,7 @@ export function createApp() { app.get("/health", (c) => c.json({ status: "ok", - service: "gittensory-api", + service: "loopover-api", time: nowIso(), minMcpVersion: MINIMUM_SUPPORTED_MCP_VERSION, latestRecommendedMcpVersion: LATEST_RECOMMENDED_MCP_VERSION, @@ -1003,7 +1003,7 @@ export function createApp() { const quality = await loadPublicRepoBadge(c.env, c.req.param("owner"), c.req.param("repo")); if (!quality) { c.header("Cache-Control", "public, max-age=300"); - return c.json({ schemaVersion: 1, label: "gittensory", message: "unavailable", color: "#9e9e9e", cacheSeconds: 300 }, 404); + return c.json({ schemaVersion: 1, label: PUBLIC_BADGE_LABEL, message: "unavailable", color: "#9e9e9e", cacheSeconds: 300 }, 404); } c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400"); return c.json(buildShieldsBadge(quality, 600)); diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 00d896473..b9011a0cf 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -2720,7 +2720,7 @@ export const AgentRunBundleSchema = z export const HealthSchema = z .object({ status: z.literal("ok"), - service: z.literal("gittensory-api"), + service: z.literal("loopover-api"), time: z.string(), minMcpVersion: z.string(), latestRecommendedMcpVersion: z.string(), @@ -2730,7 +2730,7 @@ export const HealthSchema = z export const McpCompatibilitySchema = z .object({ status: z.literal("ok"), - service: z.literal("gittensory-api"), + service: z.literal("loopover-api"), apiVersion: z.string(), mcp: z.object({ packageName: z.string(), diff --git a/src/services/mcp-compatibility.ts b/src/services/mcp-compatibility.ts index 79bd85706..cbb79b450 100644 --- a/src/services/mcp-compatibility.ts +++ b/src/services/mcp-compatibility.ts @@ -25,7 +25,7 @@ export type BreakingChangeNotice = { export type McpCompatibilityMetadata = { status: "ok"; - service: "gittensory-api"; + service: "loopover-api"; apiVersion: string; mcp: { packageName: string; @@ -44,7 +44,7 @@ export type McpCompatibilityMetadata = { export function buildMcpCompatibilityMetadata(generatedAt: string): McpCompatibilityMetadata { return { status: "ok", - service: "gittensory-api", + service: "loopover-api", apiVersion: GITTENSORY_API_VERSION, mcp: { packageName: GITTENSORY_MCP_PACKAGE_NAME, diff --git a/src/services/notify-discord.ts b/src/services/notify-discord.ts index 5c0f893e7..6b69e8815 100644 --- a/src/services/notify-discord.ts +++ b/src/services/notify-discord.ts @@ -103,7 +103,7 @@ async function auditExternalNotification( ): Promise { await recordAuditEvent(env, { eventType: `external_notification.${provider}`, - actor: "gittensory", + actor: "loopover", targetKey: `${params.repoFullName}#${params.pullNumber}`, outcome, detail, @@ -125,7 +125,7 @@ export async function notifyActionToDiscord( } const meta = OUTCOME_META[params.outcome]; const body = { - username: "Gittensory", + username: "LoopOver", embeds: [ { title: `${params.repoFullName}#${params.pullNumber} · ${meta.word}`, @@ -137,7 +137,7 @@ export async function notifyActionToDiscord( { name: "PR", value: `#${params.pullNumber}`, inline: true }, ...(params.submitter ? [{ name: "Submitter", value: `@${params.submitter}`, inline: true }] : []), ], - footer: { text: `Gittensory · ${params.repoFullName}` }, + footer: { text: `LoopOver · ${params.repoFullName}` }, }, ], }; @@ -171,12 +171,12 @@ export async function deliverRecapToDiscord( const url = envString(env, "DISCORD_WEBHOOK_URL"); if (!url || !isValidDiscordWebhook(url)) { const reason = url ? "invalid_global_webhook" : "missing_global_webhook"; - await recordAuditEvent(env, { eventType: "maintainer_recap_notification.discord", actor: "gittensory", targetKey, outcome: "denied", detail: reason, metadata: auditMeta }); + await recordAuditEvent(env, { eventType: "maintainer_recap_notification.discord", actor: "loopover", targetKey, outcome: "denied", detail: reason, metadata: auditMeta }); return { sent: false, reason }; } const description = (formattedBody ?? report.summary.join("\n")).slice(0, 1800); const body = { - username: "Gittensory", + username: "LoopOver", embeds: [ { title: `Maintainer recap · ${report.repos.length} repo(s) · ${report.windowDays}d`, @@ -190,18 +190,18 @@ export async function deliverRecapToDiscord( { name: "Overrides", value: `${report.totals.gateOverrides}`, inline: true }, { name: "Reversals", value: `${report.totals.reversals}`, inline: true }, ], - footer: { text: `Gittensory · generated ${report.generatedAt}` }, + footer: { text: `LoopOver · generated ${report.generatedAt}` }, }, ], }; try { await postWebhook(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000) }, "discord"); - await recordAuditEvent(env, { eventType: "maintainer_recap_notification.discord", actor: "gittensory", targetKey, outcome: "completed", detail: "sent", metadata: auditMeta }); + await recordAuditEvent(env, { eventType: "maintainer_recap_notification.discord", actor: "loopover", targetKey, outcome: "completed", detail: "sent", metadata: auditMeta }); return { sent: true }; } catch (error) { const detail = errorMessage(error).slice(0, 160); console.warn(JSON.stringify({ event: "maintainer_recap_discord_failed", message: detail })); - await recordAuditEvent(env, { eventType: "maintainer_recap_notification.discord", actor: "gittensory", targetKey, outcome: "error", detail, metadata: auditMeta }); + await recordAuditEvent(env, { eventType: "maintainer_recap_notification.discord", actor: "loopover", targetKey, outcome: "error", detail, metadata: auditMeta }); return { sent: false, reason: detail }; } } @@ -221,7 +221,7 @@ export async function deliverRecapToSlack( const webhookUrl = envString(env, "SLACK_WEBHOOK_URL"); if (!webhookUrl || !isValidSlackWebhook(webhookUrl)) { const reason = webhookUrl ? "invalid_webhook" : "missing_webhook"; - await recordAuditEvent(env, { eventType: "maintainer_recap_notification.slack", actor: "gittensory", targetKey, outcome: "denied", detail: reason, metadata: auditMeta }); + await recordAuditEvent(env, { eventType: "maintainer_recap_notification.slack", actor: "loopover", targetKey, outcome: "denied", detail: reason, metadata: auditMeta }); return { sent: false, reason }; } const body = { @@ -230,12 +230,12 @@ export async function deliverRecapToSlack( }; try { await postWebhook(webhookUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000) }, "slack"); - await recordAuditEvent(env, { eventType: "maintainer_recap_notification.slack", actor: "gittensory", targetKey, outcome: "completed", detail: "sent", metadata: auditMeta }); + await recordAuditEvent(env, { eventType: "maintainer_recap_notification.slack", actor: "loopover", targetKey, outcome: "completed", detail: "sent", metadata: auditMeta }); return { sent: true }; } catch (error) { const detail = errorMessage(error).slice(0, 160); console.warn(JSON.stringify({ event: "maintainer_recap_slack_failed", message: detail })); - await recordAuditEvent(env, { eventType: "maintainer_recap_notification.slack", actor: "gittensory", targetKey, outcome: "error", detail, metadata: auditMeta }); + await recordAuditEvent(env, { eventType: "maintainer_recap_notification.slack", actor: "loopover", targetKey, outcome: "error", detail, metadata: auditMeta }); return { sent: false, reason: detail }; } } diff --git a/src/services/notify-pagerduty.ts b/src/services/notify-pagerduty.ts index 9dfec9c25..30f5f182a 100644 --- a/src/services/notify-pagerduty.ts +++ b/src/services/notify-pagerduty.ts @@ -8,7 +8,7 @@ import { errorMessage } from "../utils/json"; // var is declared on the strict Env type (same asymmetry as DISCORD_REPO_WEBHOOKS) — a free-form per-repo // JSON map isn't worth a formal interface field; the global fallbacks are, and are declared in env.d.ts. // -// ALERT FATIGUE: paging is the loudest, most disruptive channel gittensory has — unlike a Discord post or a +// ALERT FATIGUE: paging is the loudest, most disruptive channel loopover has — unlike a Discord post or a // Sentry issue, it can wake someone up. Two independent controls keep it from crying wolf, on top of // PagerDuty's own `dedup_key` coalescing (which prevents duplicate *incidents* but not duplicate *pages* for // a still-open one): @@ -127,7 +127,7 @@ async function auditPagerDutyNotification( ): Promise { await recordAuditEvent(env, { eventType: "external_notification.pagerduty", - actor: "gittensory", + actor: "loopover", targetKey: params.dedupKey, outcome, detail, @@ -175,7 +175,7 @@ export async function triggerPagerDutyIncident( const cooldownMinutes = resolvePagerDutyCooldownMinutes(env, params.repoFullName); const cooldownSinceIso = new Date(Date.now() - cooldownMinutes * 60 * 1000).toISOString(); - const recentPages = await countRecentAuditEventsForActorAndTarget(env, "gittensory", "external_notification.pagerduty", params.dedupKey, cooldownSinceIso); + const recentPages = await countRecentAuditEventsForActorAndTarget(env, "loopover", "external_notification.pagerduty", params.dedupKey, cooldownSinceIso); if (recentPages > 0) { await auditPagerDutyNotification(env, { repoFullName: params.repoFullName, dedupKey: params.dedupKey }, "denied", "cooldown_active", { cooldownMinutes }); return; @@ -191,7 +191,7 @@ export async function triggerPagerDutyIncident( dedup_key: params.dedupKey, payload: { summary: params.summary.slice(0, 1024), - source: "gittensory", + source: "loopover", severity: params.severity, timestamp: new Date().toISOString(), component: params.repoFullName, diff --git a/src/services/review-recap.ts b/src/services/review-recap.ts index 4fef950e9..c5de85924 100644 --- a/src/services/review-recap.ts +++ b/src/services/review-recap.ts @@ -144,7 +144,7 @@ export async function sendReviewRecapToDiscord(env: Env, recap: ReviewRecap): Pr if (resolved.status !== "configured") { await recordAuditEvent(env, { eventType: "review_recap_notification.discord", - actor: "gittensory", + actor: "loopover", targetKey: `review-recap:${recap.repoFullName}:${recap.windowDays}`, outcome: "denied", detail: resolved.reason, @@ -153,7 +153,7 @@ export async function sendReviewRecapToDiscord(env: Env, recap: ReviewRecap): Pr return { sent: false, reason: resolved.reason }; } const body = { - username: "Gittensory", + username: "LoopOver", embeds: [ { title: `${recap.repoFullName} · review recap (${recap.windowDays}d)`, @@ -164,7 +164,7 @@ export async function sendReviewRecapToDiscord(env: Env, recap: ReviewRecap): Pr { name: "Closed", value: String(recap.closed), inline: true }, { name: "Still open", value: String(recap.stillOpen), inline: true }, ], - footer: { text: `Gittensory · ${recap.repoFullName}` }, + footer: { text: `LoopOver · ${recap.repoFullName}` }, }, ], }; @@ -178,7 +178,7 @@ export async function sendReviewRecapToDiscord(env: Env, recap: ReviewRecap): Pr if (!response.ok) throw new Error(`discord_webhook_http_${response.status}`); await recordAuditEvent(env, { eventType: "review_recap_notification.discord", - actor: "gittensory", + actor: "loopover", targetKey: `review-recap:${recap.repoFullName}:${recap.windowDays}`, outcome: "completed", detail: "sent", @@ -190,7 +190,7 @@ export async function sendReviewRecapToDiscord(env: Env, recap: ReviewRecap): Pr console.warn(JSON.stringify({ event: "review_recap_discord_failed", repo: recap.repoFullName, message: detail })); await recordAuditEvent(env, { eventType: "review_recap_notification.discord", - actor: "gittensory", + actor: "loopover", targetKey: `review-recap:${recap.repoFullName}:${recap.windowDays}`, outcome: "error", detail, @@ -211,7 +211,7 @@ export async function deliverRecapToSlack(env: Env, recap: ReviewRecap): Promise const reason = typeof webhookUrl === "string" ? "invalid_webhook" : "missing_webhook"; await recordAuditEvent(env, { eventType: "review_recap_notification.slack", - actor: "gittensory", + actor: "loopover", targetKey: `review-recap:${recap.repoFullName}:${recap.windowDays}`, outcome: "denied", detail: reason, @@ -237,7 +237,7 @@ export async function deliverRecapToSlack(env: Env, recap: ReviewRecap): Promise if (!response.ok) throw new Error(`slack_webhook_http_${response.status}`); await recordAuditEvent(env, { eventType: "review_recap_notification.slack", - actor: "gittensory", + actor: "loopover", targetKey: `review-recap:${recap.repoFullName}:${recap.windowDays}`, outcome: "completed", detail: "sent", @@ -249,7 +249,7 @@ export async function deliverRecapToSlack(env: Env, recap: ReviewRecap): Promise console.warn(JSON.stringify({ event: "review_recap_slack_failed", repo: recap.repoFullName, message: detail })); await recordAuditEvent(env, { eventType: "review_recap_notification.slack", - actor: "gittensory", + actor: "loopover", targetKey: `review-recap:${recap.repoFullName}:${recap.windowDays}`, outcome: "error", detail, diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 4cdd34444..187e7215d 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -107,7 +107,7 @@ describe("api routes", () => { expect(health.status).toBe(200); await expect(health.json()).resolves.toMatchObject({ status: "ok", - service: "gittensory-api", + service: "loopover-api", minMcpVersion: "0.5.0", latestRecommendedMcpVersion: mcpPackageJson.version, }); @@ -117,7 +117,7 @@ describe("api routes", () => { const compatibilityPayload = await compatibility.json(); expect(compatibilityPayload).toMatchObject({ status: "ok", - service: "gittensory-api", + service: "loopover-api", apiVersion: "0.1.0", mcp: { packageName: "@jsonbored/gittensory-mcp", @@ -289,13 +289,13 @@ describe("api routes", () => { expect(svg.headers.get("cache-control")).toContain("stale-while-revalidate"); const svgBody = await svg.text(); expect(svgBody.startsWith(" { it("emits a shields endpoint payload", () => { expect(buildShieldsBadge(quality(), 600)).toEqual({ schemaVersion: 1, - label: "gittensory", + label: "loopover", message: "92% real · merge 30h · queue low", color: "#3fb950", cacheSeconds: 600, @@ -75,7 +75,7 @@ describe("renderBadgeSvg", () => { it("renders a valid SVG carrying the label and message", () => { const svg = renderBadgeSvg(quality()); expect(svg.startsWith(" { routing_key: VALID_KEY, event_action: "trigger", dedup_key: "ops_anomaly:acme/widgets", - payload: { summary: "review burst on acme/widgets", source: "gittensory", severity: "critical", component: "acme/widgets", custom_details: { anomalies: ["a", "b"] } }, + payload: { summary: "review burst on acme/widgets", source: "loopover", severity: "critical", component: "acme/widgets", custom_details: { anomalies: ["a", "b"] } }, }); expect(await pagerDutyAudit(env)).toEqual([expect.objectContaining({ outcome: "completed", detail: "triggered" })]); }); diff --git a/test/unit/support/mcp-cli-harness.ts b/test/unit/support/mcp-cli-harness.ts index 2e2a3b557..271c7121c 100644 --- a/test/unit/support/mcp-cli-harness.ts +++ b/test/unit/support/mcp-cli-harness.ts @@ -140,7 +140,7 @@ export async function startFixtureServer( response.end( JSON.stringify({ status: "ok", - service: "gittensory-api", + service: "loopover-api", apiVersion: "0.1.0", mcp: { packageName: "@jsonbored/gittensory-mcp", @@ -159,7 +159,7 @@ export async function startFixtureServer( return; } if (request.url === "/health") { - response.end(JSON.stringify({ status: "ok", service: "gittensory-api", ...(options.minMcpVersion ? { minMcpVersion: options.minMcpVersion } : {}) })); + response.end(JSON.stringify({ status: "ok", service: "loopover-api", ...(options.minMcpVersion ? { minMcpVersion: options.minMcpVersion } : {}) })); return; } if (request.url === "/v1/auth/github/session" && request.method === "POST") { diff --git a/test/workers/worker-runtime.test.ts b/test/workers/worker-runtime.test.ts index 214772928..1fa97aaf8 100644 --- a/test/workers/worker-runtime.test.ts +++ b/test/workers/worker-runtime.test.ts @@ -8,7 +8,7 @@ describe("worker runtime", () => { const health = await worker.fetch(new Request("https://gittensory.test/health"), {} as Env, ctx); await waitOnExecutionContext(ctx); expect(health.status).toBe(200); - await expect(health.json()).resolves.toMatchObject({ status: "ok", service: "gittensory-api" }); + await expect(health.json()).resolves.toMatchObject({ status: "ok", service: "loopover-api" }); const openApi = await worker.fetch(new Request("https://gittensory.test/openapi.json"), {} as Env, createExecutionContext()); expect(openApi.status).toBe(200); From 0eb6b6d0a3317f3b478465d124544668345e1d74 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:27:38 -0700 Subject: [PATCH 2/2] fix(pagerduty): survive the actor rename across the cooldown window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paging cooldown check queried only the new "loopover" actor, so a page recorded under the pre-rebrand "gittensory" actor just before this deploy would no longer suppress a duplicate page after it, for as long as the configured cooldown window reaches back across the deploy boundary. Count both the current and legacy actor and sum the results — one extra indexed count, but it removes the whole risk category instead of requiring a precisely-timed follow-up cleanup. --- src/services/notify-pagerduty.ts | 10 +++++++++- test/unit/notify-pagerduty.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/services/notify-pagerduty.ts b/src/services/notify-pagerduty.ts index 30f5f182a..7d1ff49b9 100644 --- a/src/services/notify-pagerduty.ts +++ b/src/services/notify-pagerduty.ts @@ -175,7 +175,15 @@ export async function triggerPagerDutyIncident( const cooldownMinutes = resolvePagerDutyCooldownMinutes(env, params.repoFullName); const cooldownSinceIso = new Date(Date.now() - cooldownMinutes * 60 * 1000).toISOString(); - const recentPages = await countRecentAuditEventsForActorAndTarget(env, "loopover", "external_notification.pagerduty", params.dedupKey, cooldownSinceIso); + // Also count the pre-rebrand "gittensory" actor: a page recorded under the OLD actor value just before this + // rebrand deployed must still suppress a duplicate page after it, for as long as the configured cooldown + // window can reach back across the deploy boundary. Querying both actors costs one extra indexed count and + // removes the whole risk category rather than requiring a precisely-timed follow-up cleanup. + const [recentPagesNewActor, recentPagesLegacyActor] = await Promise.all([ + countRecentAuditEventsForActorAndTarget(env, "loopover", "external_notification.pagerduty", params.dedupKey, cooldownSinceIso), + countRecentAuditEventsForActorAndTarget(env, "gittensory", "external_notification.pagerduty", params.dedupKey, cooldownSinceIso), + ]); + const recentPages = recentPagesNewActor + recentPagesLegacyActor; if (recentPages > 0) { await auditPagerDutyNotification(env, { repoFullName: params.repoFullName, dedupKey: params.dedupKey }, "denied", "cooldown_active", { cooldownMinutes }); return; diff --git a/test/unit/notify-pagerduty.test.ts b/test/unit/notify-pagerduty.test.ts index 45b5d383b..f5acae6cb 100644 --- a/test/unit/notify-pagerduty.test.ts +++ b/test/unit/notify-pagerduty.test.ts @@ -228,6 +228,25 @@ describe("triggerPagerDutyIncident — cooldown gate (alert fatigue control #2)" await trigger(env); // default cooldown is 60 minutes; the seeded row is 2 hours old expect(calls).toHaveLength(1); }); + + it("REGRESSION: a recent page recorded under the pre-rebrand 'gittensory' actor still suppresses a duplicate within the cooldown window", async () => { + const calls = stubFetch(); + const env = enabledEnv(); + const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString(); + await recordAuditEvent(env, { + eventType: "external_notification.pagerduty", + actor: "gittensory", + targetKey: "ops_anomaly:acme/widgets", + outcome: "completed", + detail: "triggered", + metadata: {}, + createdAt: tenMinutesAgo, + }); + await trigger(env); // default cooldown is 60 minutes; the legacy-actor row is only 10 minutes old + expect(calls).toHaveLength(0); + const rows = await pagerDutyAudit(env); + expect(rows[rows.length - 1]).toEqual(expect.objectContaining({ outcome: "denied", detail: "cooldown_active" })); + }); }); describe("triggerPagerDutyIncident — HTTP delivery", () => {