diff --git a/tests/unit/web/triggerAgentMapping.test.ts b/tests/unit/web/triggerAgentMapping.test.ts new file mode 100644 index 00000000..a9fb0198 --- /dev/null +++ b/tests/unit/web/triggerAgentMapping.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest'; +import { getTriggersForAgent } from '../../../web/src/lib/trigger-agent-mapping.js'; + +describe('getTriggersForAgent', () => { + it('returns all triggers when no opts given (backward compatibility)', () => { + const triggers = getTriggersForAgent('review'); + expect(triggers).toHaveLength(3); + expect(triggers.map((t) => t.key)).toEqual([ + 'reviewTrigger.ownPrsOnly', + 'reviewTrigger.externalPrs', + 'reviewTrigger.onReviewRequested', + ]); + }); + + it('returns empty array for review agent with category: pm', () => { + const triggers = getTriggersForAgent('review', { category: 'pm' }); + expect(triggers).toHaveLength(0); + }); + + it('returns 3 review triggers for review agent with category: scm', () => { + const triggers = getTriggersForAgent('review', { category: 'scm' }); + expect(triggers).toHaveLength(3); + for (const t of triggers) { + expect(t.category).toBe('scm'); + } + }); + + it('returns PM-only triggers for briefing with category: pm and pmProvider: trello', () => { + const triggers = getTriggersForAgent('briefing', { category: 'pm', pmProvider: 'trello' }); + expect(triggers.length).toBeGreaterThan(0); + for (const t of triggers) { + expect(t.category).toBe('pm'); + // Should not include JIRA-only triggers + if (t.pmProvider) { + expect(t.pmProvider).toBe('trello'); + } + } + const keys = triggers.map((t) => t.key); + expect(keys).toContain('cardMovedToBriefing'); + expect(keys).toContain('readyToProcessLabel.briefing'); + expect(keys).not.toContain('issueTransitioned.briefing'); + }); + + it('returns empty array for briefing with category: scm', () => { + const triggers = getTriggersForAgent('briefing', { category: 'scm' }); + expect(triggers).toHaveLength(0); + }); + + it('filters by pmProvider without category', () => { + const jiraTriggers = getTriggersForAgent('briefing', { pmProvider: 'jira' }); + const trelloTriggers = getTriggersForAgent('briefing', { pmProvider: 'trello' }); + // JIRA provider should exclude cardMovedToBriefing (trello-only) + expect(jiraTriggers.map((t) => t.key)).not.toContain('cardMovedToBriefing'); + // Trello provider should exclude issueTransitioned.briefing (jira-only) + expect(trelloTriggers.map((t) => t.key)).not.toContain('issueTransitioned.briefing'); + }); + + it('returns empty array for unknown agent type', () => { + const triggers = getTriggersForAgent('unknown-agent', { category: 'pm' }); + expect(triggers).toHaveLength(0); + }); +}); + +describe('getTriggersForAgent — review trigger dot-notation keys and defaults', () => { + it('returns dot-notation keys for review SCM triggers', () => { + const triggerDefs = getTriggersForAgent('review', { category: 'scm' }); + + // Verify that the trigger definitions have the expected dot-notation keys + expect(triggerDefs.map((t) => t.key)).toEqual([ + 'reviewTrigger.ownPrsOnly', + 'reviewTrigger.externalPrs', + 'reviewTrigger.onReviewRequested', + ]); + + // Verify each trigger has the correct category + for (const t of triggerDefs) { + expect(t.category).toBe('scm'); + } + }); + + it('returns correct defaultValues for review triggers', () => { + const triggers = getTriggersForAgent('review', { category: 'scm' }); + const defaults = Object.fromEntries(triggers.map((t) => [t.key, t.defaultValue])); + expect(defaults['reviewTrigger.ownPrsOnly']).toBe(false); + expect(defaults['reviewTrigger.externalPrs']).toBe(false); + expect(defaults['reviewTrigger.onReviewRequested']).toBe(false); + }); +}); diff --git a/web/src/components/projects/project-agent-configs.tsx b/web/src/components/projects/project-agent-configs.tsx index 8939a60a..4e242fab 100644 --- a/web/src/components/projects/project-agent-configs.tsx +++ b/web/src/components/projects/project-agent-configs.tsx @@ -115,8 +115,8 @@ function AgentSection({ const [pmSaved, setPmSaved] = useState(false); const [scmSaved, setScmSaved] = useState(false); - const agentPmTriggers = getTriggersForAgent(agentType, pmProvider); - const agentScmTriggers = getTriggersForAgent(agentType).filter((t) => t.category === 'scm'); + const agentPmTriggers = getTriggersForAgent(agentType, { pmProvider, category: 'pm' }); + const agentScmTriggers = getTriggersForAgent(agentType, { category: 'scm' }); const hasTriggers = agentPmTriggers.length > 0 || agentScmTriggers.length > 0; @@ -144,10 +144,7 @@ function AgentSection({ const handleSaveScm = async () => { setScmSaving(true); try { - const relevant: Record = {}; - for (const t of agentScmTriggers) { - relevant[t.key] = localScmTriggers[t.key] ?? t.defaultValue; - } + const relevant = extractRelevantTriggers(agentScmTriggers, localScmTriggers); await onSaveTriggers('scm', relevant, agentType); setScmSaved(true); setTimeout(() => setScmSaved(false), 2000); diff --git a/web/src/lib/trigger-agent-mapping.ts b/web/src/lib/trigger-agent-mapping.ts index b7cdda9c..ac4f655a 100644 --- a/web/src/lib/trigger-agent-mapping.ts +++ b/web/src/lib/trigger-agent-mapping.ts @@ -211,12 +211,16 @@ export const AGENT_TRIGGER_MAP: Record = { }; /** - * Get trigger definitions for a specific agent type, filtered by PM provider. + * Get trigger definitions for a specific agent type, filtered by PM provider and/or category. */ -export function getTriggersForAgent(agentType: string, pmProvider?: string): TriggerDef[] { +export function getTriggersForAgent( + agentType: string, + opts?: { pmProvider?: string; category?: 'pm' | 'scm' }, +): TriggerDef[] { const triggers = AGENT_TRIGGER_MAP[agentType] ?? []; return triggers.filter((t) => { - if (t.pmProvider && pmProvider && t.pmProvider !== pmProvider) return false; + if (opts?.category && t.category !== opts.category) return false; + if (t.pmProvider && opts?.pmProvider && t.pmProvider !== opts.pmProvider) return false; return true; }); }