Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

207 lines
8.5 KiB
TypeScript

import { PropertyFilterType, PropertyOperator } from '~/types'
import {
PendingAlertNotification,
buildAlertFilterConfig,
buildInsightsFunctionPayload,
getAlertNotificationAddDisabledReason,
notificationTypeFromTemplateId,
} from './alertNotifications'
describe('alertUtils', () => {
describe('getAlertNotificationAddDisabledReason', () => {
it.each([
['webhook', 'Enter a valid webhook URL'],
['discord', 'Enter a valid Discord webhook URL'],
['microsoft_teams', 'Enter a valid Microsoft Teams workflow URL'],
] as const)('rejects an invalid %s URL', (selectedType, expectedReason) => {
expect(getAlertNotificationAddDisabledReason(selectedType, false, null, 'not a url')).toBe(expectedReason)
})
it('accepts a valid webhook URL', () => {
expect(
getAlertNotificationAddDisabledReason('webhook', false, null, 'https://example.com/webhook')
).toBeUndefined()
})
})
describe('buildAlertFilterConfig', () => {
it('builds filter config with the correct event and alert_id property', () => {
const result = buildAlertFilterConfig('alert-123')
expect(result).toEqual({
properties: [
{
key: 'alert_id',
value: 'alert-123',
operator: PropertyOperator.Exact,
type: PropertyFilterType.Event,
},
],
events: [
{
id: '$insight_alert_firing',
type: 'events',
},
],
})
})
})
describe('buildInsightsFunctionPayload', () => {
it.each([
{
name: 'slack notification with alert name',
notification: {
type: 'slack' as const,
slackWorkspaceId: 42,
slackChannelId: 'C12345',
slackChannelName: 'general',
},
alertName: 'Daily revenue check',
expectedName: 'Daily revenue check: Slack #general',
expectedTemplateId: 'template-slack',
expectedInputKeys: ['blocks', 'text', 'slack_workspace', 'channel'],
},
{
name: 'slack notification without alert name',
notification: {
type: 'slack' as const,
slackWorkspaceId: 42,
slackChannelId: 'C12345',
},
alertName: undefined,
expectedName: 'Alert: Slack #channel',
expectedTemplateId: 'template-slack',
expectedInputKeys: ['blocks', 'text', 'slack_workspace', 'channel'],
},
{
name: 'webhook notification',
notification: {
type: 'webhook' as const,
webhookUrl: 'https://example.com/hook',
},
alertName: 'Spike detector',
expectedName: 'Spike detector: Webhook https://example.com/hook',
expectedTemplateId: 'template-webhook',
expectedInputKeys: ['url', 'body'],
},
{
name: 'discord notification',
notification: {
type: 'discord' as const,
webhookUrl: 'https://discord.com/api/webhooks/123/abc',
},
alertName: 'Spike detector',
expectedName: 'Spike detector: Discord',
expectedTemplateId: 'template-discord',
expectedInputKeys: ['content', 'webhookUrl'],
},
{
name: 'microsoft teams notification',
notification: {
type: 'microsoft_teams' as const,
webhookUrl: 'https://example.webhook.office.com/webhookb2/abc',
},
alertName: 'Spike detector',
expectedName: 'Spike detector: Microsoft Teams',
expectedTemplateId: 'template-microsoft-teams',
expectedInputKeys: ['text', 'webhookUrl'],
},
])(
'builds correct payload for $name',
({ notification, alertName, expectedName, expectedTemplateId, expectedInputKeys }) => {
const result = buildInsightsFunctionPayload('alert-456', alertName, notification as PendingAlertNotification)
expect(result.name).toBe(expectedName)
expect(result.template_id).toBe(expectedTemplateId)
expect(result.type).toBe('internal_destination')
expect(result.enabled).toBe(true)
expect(result.masking).toBeNull()
expect(result.filters).toEqual(buildAlertFilterConfig('alert-456'))
expect(Object.keys(result.inputs ?? {})).toEqual(expect.arrayContaining(expectedInputKeys))
}
)
it('sets correct slack input values', () => {
const notification: PendingAlertNotification = {
type: 'slack',
slackWorkspaceId: 42,
slackChannelId: 'C12345',
slackChannelName: 'general',
}
const result = buildInsightsFunctionPayload('alert-789', 'My alert', notification)
expect(result.inputs?.slack_workspace).toEqual({ value: 42 })
expect(result.inputs?.channel).toEqual({ value: 'C12345' })
})
it('sets correct discord input values', () => {
const notification: PendingAlertNotification = {
type: 'discord',
webhookUrl: 'https://discord.com/api/webhooks/123/abc',
}
const result = buildInsightsFunctionPayload('alert-789', 'My alert', notification)
expect(result.inputs?.webhookUrl).toEqual({ value: 'https://discord.com/api/webhooks/123/abc' })
expect(result.inputs?.content).toEqual({
value: expect.stringContaining('{event.properties.alert_name}'),
})
})
it('sets correct microsoft teams input values', () => {
const notification: PendingAlertNotification = {
type: 'microsoft_teams',
webhookUrl: 'https://example.webhook.office.com/webhookb2/abc',
}
const result = buildInsightsFunctionPayload('alert-789', 'My alert', notification)
expect(result.inputs?.webhookUrl).toEqual({ value: 'https://example.webhook.office.com/webhookb2/abc' })
expect(result.inputs?.text).toEqual({
value: expect.stringContaining('{event.properties.alert_name}'),
})
})
it('sets correct webhook input values', () => {
const notification: PendingAlertNotification = {
type: 'webhook',
webhookUrl: 'https://example.com/hook',
}
const result = buildInsightsFunctionPayload('alert-789', 'My alert', notification)
expect(result.inputs?.url).toEqual({ value: 'https://example.com/hook' })
expect(result.inputs?.body).toEqual({
value: {
alert_name: '{event.properties.alert_name}',
insight_name: '{event.properties.insight_name}',
breaches: '{event.properties.breaches}',
insight_url: '{project.url}/insights/{event.properties.insight_id}',
alert_url:
'{project.url}/insights/{event.properties.insight_id}/alerts?alert_id={event.properties.alert_id}',
},
})
})
})
describe('notificationTypeFromTemplateId', () => {
// Guards the create/delete analytics events: buildAlertDestination writes these template_ids,
// and this maps them back to the type we report — a drift between the two would mislabel adoption.
it.each([
['template-slack', 'slack'],
['template-discord', 'discord'],
['template-microsoft-teams', 'microsoft_teams'],
['template-webhook', 'webhook'],
])('maps %s to %s', (templateId, expected) => {
expect(notificationTypeFromTemplateId(templateId)).toEqual(expected)
})
it.each([[undefined], [null], ['template-unknown']])('returns null for %s', (templateId) => {
expect(notificationTypeFromTemplateId(templateId)).toBeNull()
})
})
})