Files
kongruity/backend/tests/slack.normalizer.test.ts
2026-08-02 06:13:01 -04:00

246 lines
8.8 KiB
TypeScript

import { describe, it, expect, afterEach } from 'vitest';
import {
NormalizationError,
SLACK_PENDING_TEXT,
isNoteInputArray,
normalizeSlack,
} from '../config/normalizers.js';
import type { NoteInput } from '../types/domain.js';
const sourceMeta = (input: NoteInput): Record<string, unknown> => {
expect(input.sourceMeta).toBeDefined();
return input.sourceMeta as Record<string, unknown>;
};
const slackPayload = (
event: Record<string, unknown> = {},
envelope: Record<string, unknown> = {}
): Record<string, unknown> => ({
type: 'event_callback',
event_id: 'Ev08K1QR2X',
team_id: 'T0123',
event: {
type: 'reaction_added',
user: 'U_REACTOR',
reaction: 'pushpin',
item_user: 'U_AUTHOR',
item: { type: 'message', channel: 'C0123', ts: '1700000000.000100' },
event_ts: '1700000001.000200',
...event,
},
...envelope,
});
const item = (overrides: Record<string, unknown> = {}): Record<string, unknown> => ({
type: 'message',
channel: 'C0123',
ts: '1700000000.000100',
...overrides,
});
const savedReaction = process.env.SLACK_CAPTURE_REACTION;
const setCaptureReaction = (value: string | undefined): void => {
if (value === undefined) {
delete process.env.SLACK_CAPTURE_REACTION;
return;
}
process.env.SLACK_CAPTURE_REACTION = value;
};
afterEach(() => {
setCaptureReaction(savedReaction);
});
describe('normalizeSlack', () => {
it('should map a captured reaction to a single placeholder note', () => {
const result = normalizeSlack(slackPayload());
expect(result.externalId).toBe('Ev08K1QR2X');
expect(result.notes).toHaveLength(1);
expect(result.notes[0].id).toBe('slack_Ev08K1QR2X');
expect(result.notes[0].text).toBe(SLACK_PENDING_TEXT);
expect(result.notes[0].author).toBe('U_AUTHOR');
});
it('should export the placeholder text used for the pending message body', () => {
expect(SLACK_PENDING_TEXT).toBe('(pending Slack message text)');
});
it('should attach provenance plus the identifiers the enrichment hook needs', () => {
const meta = sourceMeta(normalizeSlack(slackPayload()).notes[0]);
expect(meta).toMatchObject({
provider: 'slack',
externalId: 'Ev08K1QR2X',
needsMessageText: true,
channelId: 'C0123',
messageTs: '1700000000.000100',
teamId: 'T0123',
reaction: 'pushpin',
reactedBy: 'U_REACTOR',
authorUserId: 'U_AUTHOR',
});
expect(typeof meta.receivedAt).toBe('string');
});
it('should mark needsMessageText as exactly true', () => {
const meta = sourceMeta(normalizeSlack(slackPayload()).notes[0]);
expect(meta.needsMessageText).toBe(true);
});
it('should record an ISO receivedAt timestamp in provenance', () => {
const receivedAt = String(sourceMeta(normalizeSlack(slackPayload()).notes[0]).receivedAt);
expect(new Date(receivedAt).toISOString()).toBe(receivedAt);
});
it('should fall back to the reacting user when item_user is absent', () => {
expect(normalizeSlack(slackPayload({ item_user: undefined })).notes[0].author).toBe(
'U_REACTOR'
);
expect(normalizeSlack(slackPayload({ item_user: '' })).notes[0].author).toBe('U_REACTOR');
});
it('should fall back to an unknown author when neither user is present', () => {
const result = normalizeSlack(slackPayload({ item_user: undefined, user: undefined }));
expect(result.notes[0].author).toBe('unknown');
expect(sourceMeta(result.notes[0]).reactedBy).toBeUndefined();
expect(sourceMeta(result.notes[0]).authorUserId).toBeUndefined();
});
it('should omit teamId when the envelope carries none', () => {
const payload = slackPayload({}, { team_id: undefined });
expect(sourceMeta(normalizeSlack(payload).notes[0]).teamId).toBeUndefined();
});
it('should return zero notes for a reaction other than the configured one', () => {
expect(normalizeSlack(slackPayload({ reaction: 'eyes' }))).toEqual({
externalId: 'Ev08K1QR2X',
notes: [],
});
});
it('should honour a custom capture reaction from the environment', () => {
setCaptureReaction('thumbsup');
expect(normalizeSlack(slackPayload({ reaction: 'thumbsup' })).notes).toHaveLength(1);
expect(normalizeSlack(slackPayload({ reaction: 'pushpin' })).notes).toHaveLength(0);
});
it('should read the capture reaction at call time rather than at module load', () => {
setCaptureReaction('eyes');
expect(normalizeSlack(slackPayload({ reaction: 'eyes' })).notes).toHaveLength(1);
setCaptureReaction('rocket');
expect(normalizeSlack(slackPayload({ reaction: 'eyes' })).notes).toHaveLength(0);
expect(normalizeSlack(slackPayload({ reaction: 'rocket' })).notes).toHaveLength(1);
});
it('should match a reaction carrying a skin-tone modifier', () => {
setCaptureReaction('thumbsup');
const result = normalizeSlack(slackPayload({ reaction: 'thumbsup::skin-tone-3' }));
expect(result.notes).toHaveLength(1);
expect(sourceMeta(result.notes[0]).reaction).toBe('thumbsup::skin-tone-3');
});
it('should fall back to pushpin when no capture reaction is configured', () => {
setCaptureReaction(undefined);
expect(normalizeSlack(slackPayload()).notes).toHaveLength(1);
expect(normalizeSlack(slackPayload({ reaction: 'pushpin::skin-tone-5' })).notes).toHaveLength(
1
);
});
it('should return zero notes when the reaction is missing or not a string', () => {
expect(normalizeSlack(slackPayload({ reaction: undefined })).notes).toHaveLength(0);
expect(normalizeSlack(slackPayload({ reaction: 7 })).notes).toHaveLength(0);
expect(normalizeSlack(slackPayload({ reaction: '' })).notes).toHaveLength(0);
});
it('should return zero notes for an event type we do not capture', () => {
expect(normalizeSlack(slackPayload({ type: 'message' }))).toEqual({
externalId: 'Ev08K1QR2X',
notes: [],
});
expect(normalizeSlack(slackPayload({ type: 'reaction_removed' })).notes).toHaveLength(0);
});
it('should return zero notes when the event is missing or not an object', () => {
expect(normalizeSlack({ type: 'event_callback', event_id: 'Ev1' })).toEqual({
externalId: 'Ev1',
notes: [],
});
expect(normalizeSlack({ type: 'event_callback', event_id: 'Ev1', event: 'nope' })).toEqual({
externalId: 'Ev1',
notes: [],
});
});
it('should return zero notes when the reacted item is not a message', () => {
expect(normalizeSlack(slackPayload({ item: item({ type: 'file' }) }))).toEqual({
externalId: 'Ev08K1QR2X',
notes: [],
});
});
it('should return zero notes when the item channel is missing', () => {
expect(normalizeSlack(slackPayload({ item: item({ channel: undefined }) })).notes).toHaveLength(
0
);
expect(normalizeSlack(slackPayload({ item: item({ channel: '' }) })).notes).toHaveLength(0);
});
it('should return zero notes when the item timestamp is missing', () => {
expect(normalizeSlack(slackPayload({ item: item({ ts: undefined }) })).notes).toHaveLength(0);
expect(normalizeSlack(slackPayload({ item: item({ ts: 1700000000 }) })).notes).toHaveLength(0);
});
it('should return zero notes when the item itself is missing', () => {
expect(normalizeSlack(slackPayload({ item: undefined })).notes).toHaveLength(0);
});
it('should throw when event_id is missing or empty', () => {
expect(() => normalizeSlack(slackPayload({}, { event_id: undefined }))).toThrow(
NormalizationError
);
expect(() => normalizeSlack(slackPayload({}, { event_id: '' }))).toThrow(/missing event_id/);
expect(() => normalizeSlack(slackPayload({}, { event_id: 42 }))).toThrow(NormalizationError);
});
it('should throw for an envelope type other than event_callback', () => {
expect(() => normalizeSlack(slackPayload({}, { type: 'url_verification' }))).toThrow(
NormalizationError
);
expect(() => normalizeSlack(slackPayload({}, { type: undefined }))).toThrow(
/not an event_callback/
);
});
it('should throw for a non-object payload', () => {
expect(() => normalizeSlack(null)).toThrow(/not an object/);
expect(() => normalizeSlack(undefined)).toThrow(NormalizationError);
expect(() => normalizeSlack('event_callback')).toThrow(NormalizationError);
expect(() => normalizeSlack([slackPayload()])).toThrow(/not an object/);
});
it('should truncate a generated note id to 64 characters', () => {
const longId = 'E'.repeat(400);
const result = normalizeSlack(slackPayload({}, { event_id: longId }));
expect(result.externalId).toBe(longId);
expect(result.notes[0].id).toHaveLength(64);
expect(result.notes[0].id).toBe(`slack_${longId}`.slice(0, 64));
expect(sourceMeta(result.notes[0]).externalId).toBe(longId);
});
it('should produce notes accepted by the note input guard', () => {
expect(isNoteInputArray(normalizeSlack(slackPayload()).notes)).toBe(true);
});
});