Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, it, expect, jest, afterEach } from '@jest/globals'
import { toManifestDocument } from './guildAutomationHelpers'
import * as log from '../../utils/general/log'

// Mock the errorLog function
jest.mock('../../utils/general/log', () => ({
errorLog: jest.fn(),
}))

describe('guildAutomationHelpers', () => {
afterEach(() => {
jest.clearAllMocks()
})

describe('toManifestDocument', () => {
it('parses a valid manifest document', () => {
const validManifest = {
version: 1,
guild: {
id: '123456789012345678',
name: 'Test Guild',
},
source: 'manual',
}

const result = toManifestDocument(validManifest)

expect(result.version).toBe(1)
expect(result.guild.id).toBe('123456789012345678')
})

it('throws on malformed manifest JSON (missing required guild field)', () => {
const malformedManifest = {
version: 1,
}

expect(() => toManifestDocument(malformedManifest)).toThrow(
'Invalid manifest',
)
expect(log.errorLog).toHaveBeenCalled()
})

it('throws on invalid guild snowflake', () => {
const invalidGuildId = {
version: 1,
guild: {
id: 'not-a-snowflake',
name: 'Test Guild',
},
}

expect(() => toManifestDocument(invalidGuildId)).toThrow(
'Invalid manifest',
)
expect(log.errorLog).toHaveBeenCalled()
})

it('throws when value is not an object', () => {
expect(() => toManifestDocument('not an object')).toThrow(
'Manifest payload is invalid',
)
})

it('throws when value is null', () => {
expect(() => toManifestDocument(null)).toThrow(
'Manifest payload is invalid',
)
})

it('throws when value is an array', () => {
expect(() => toManifestDocument([1, 2, 3])).toThrow(
'Manifest payload is invalid',
)
})

it('logs validation errors with path and message', () => {
const malformedManifest = {
version: 1,
guild: {
id: '999', // Too short for snowflake
name: 'Test Guild',
},
}

expect(() => toManifestDocument(malformedManifest)).toThrow()

expect(log.errorLog).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Failed to parse GuildAutomationManifest JSON',
error: expect.any(Array),
}),
)

const errorCall = (log.errorLog as jest.Mock).mock.calls[0]?.[0] as {
error: Array<{ path: string; message: string; code: string }>
}
expect(errorCall.error).toBeInstanceOf(Array)
expect(errorCall.error[0]).toHaveProperty('path')
expect(errorCall.error[0]).toHaveProperty('message')
expect(errorCall.error[0]).toHaveProperty('code')
})
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Prisma } from '../../generated/prisma/client.js'
import type { GuildAutomationManifestDocument } from './types.js'
import { guildAutomationManifestSchema } from './manifestSchema.js'
import { errorLog } from '../../utils/general/log.js'

export function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
Expand All @@ -13,7 +14,24 @@ export function toManifestDocument(
throw new Error('Manifest payload is invalid')
}

return guildAutomationManifestSchema.parse(value)
const result = guildAutomationManifestSchema.safeParse(value)

if (!result.success) {
const errors = result.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
code: issue.code,
}))
errorLog({
message: 'Failed to parse GuildAutomationManifest JSON',
error: errors,
})
throw new Error(
`Invalid manifest: ${result.error.issues[0]?.message ?? 'unknown error'}`,
)
}

return result.data
}

export function toJsonValue(value: unknown): Prisma.InputJsonValue {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Add compound index on (guildId, discordUserId, trackId) to support
-- deduplication queries for event-log semantics in recommendation tracking.
-- This index allows efficient identification of duplicate recommendation
-- events without enforcing uniqueness (multiple recommendations of the same
-- track to the same user over time are intentional and tracked separately).
-- See issue #1194 for decision on event-log vs. uniqueness trade-off.

CREATE INDEX "recommendations_guildId_discordUserId_trackId_idx"
ON "recommendations"("guildId", "discordUserId", "trackId");
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ model Recommendation {
@@index([guildId, createdAt])
@@index([guildId, source, createdAt])
@@index([guildId, mode, createdAt])
@@index([guildId, discordUserId, trackId])
@@map("recommendations")
}

Expand Down
Loading