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
10 changes: 10 additions & 0 deletions src/triggers/github/pr-opened.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { resolveGitHubTriggerEnabled } from '../../config/triggerConfig.js';
import { isCascadeBot } from '../../github/personas.js';
import type { TriggerContext, TriggerHandler, TriggerResult } from '../../types/index.js';
import { logger } from '../../utils/logging.js';
import { isGitHubPullRequestPayload } from './types.js';
Expand Down Expand Up @@ -38,11 +39,20 @@ export class PROpenedTrigger implements TriggerHandler {
body: string | null;
html_url: string;
head: { ref: string };
user: { login: string };
};
repository: { full_name: string };
};

const prNumber = payload.pull_request.number;
const prAuthor = payload.pull_request.user.login;

// Skip PRs authored by CASCADE bots — nothing to "respond to" on our own PRs
if (ctx.personaIdentities && isCascadeBot(prAuthor, ctx.personaIdentities)) {
logger.info('Skipping PR opened by CASCADE bot', { prNumber, prAuthor });
return null;
}

const prBody = payload.pull_request.body || '';

// Resolve work item from DB (with PR body fallback)
Expand Down
139 changes: 139 additions & 0 deletions tests/unit/triggers/pr-opened.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,145 @@ describe('PROpenedTrigger', () => {
expect(result?.workItemId).toBeUndefined();
});

it('returns null for PRs by implementer persona', async () => {
const ctx: TriggerContext = {
project: mockProjectWithPrOpenedEnabled,
source: 'github',
personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-review' },
payload: {
action: 'opened',
number: 42,
pull_request: {
number: 42,
title: 'feat: add login',
body: 'Implements feature',
html_url: 'https://github.com/owner/repo/pull/42',
state: 'open',
draft: false,
head: { ref: 'feature/login', sha: 'abc' },
base: { ref: 'main' },
user: { login: 'cascade-impl' },
},
repository: { full_name: 'owner/repo', html_url: 'https://github.com/owner/repo' },
sender: { login: 'cascade-impl' },
},
};

expect(await trigger.handle(ctx)).toBeNull();
});

it('returns null for PRs by reviewer persona', async () => {
const ctx: TriggerContext = {
project: mockProjectWithPrOpenedEnabled,
source: 'github',
personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-review' },
payload: {
action: 'opened',
number: 42,
pull_request: {
number: 42,
title: 'feat: add login',
body: 'Implements feature',
html_url: 'https://github.com/owner/repo/pull/42',
state: 'open',
draft: false,
head: { ref: 'feature/login', sha: 'abc' },
base: { ref: 'main' },
user: { login: 'cascade-review' },
},
repository: { full_name: 'owner/repo', html_url: 'https://github.com/owner/repo' },
sender: { login: 'cascade-review' },
},
};

expect(await trigger.handle(ctx)).toBeNull();
});

it('returns null for [bot] variant', async () => {
const ctx: TriggerContext = {
project: mockProjectWithPrOpenedEnabled,
source: 'github',
personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-review' },
payload: {
action: 'opened',
number: 42,
pull_request: {
number: 42,
title: 'feat: add login',
body: 'Implements feature',
html_url: 'https://github.com/owner/repo/pull/42',
state: 'open',
draft: false,
head: { ref: 'feature/login', sha: 'abc' },
base: { ref: 'main' },
user: { login: 'cascade-impl[bot]' },
},
repository: { full_name: 'owner/repo', html_url: 'https://github.com/owner/repo' },
sender: { login: 'cascade-impl[bot]' },
},
};

expect(await trigger.handle(ctx)).toBeNull();
});

it('fires normally for external PRs with personaIdentities present', async () => {
const ctx: TriggerContext = {
project: mockProjectWithPrOpenedEnabled,
source: 'github',
personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-review' },
payload: {
action: 'opened',
number: 42,
pull_request: {
number: 42,
title: 'Test PR',
body: 'Just a regular PR',
html_url: 'https://github.com/owner/repo/pull/42',
state: 'open',
draft: false,
head: { ref: 'feature/test', sha: 'abc' },
base: { ref: 'main' },
user: { login: 'external-dev' },
},
repository: { full_name: 'owner/repo', html_url: 'https://github.com/owner/repo' },
sender: { login: 'external-dev' },
},
};

const result = await trigger.handle(ctx);
expect(result).not.toBeNull();
expect(result?.agentType).toBe('respond-to-review');
});

it('fires normally without personaIdentities (graceful degradation)', async () => {
const ctx: TriggerContext = {
project: mockProjectWithPrOpenedEnabled,
source: 'github',
// no personaIdentities — credential resolution failed
payload: {
action: 'opened',
number: 42,
pull_request: {
number: 42,
title: 'Test PR',
body: 'Just a regular PR',
html_url: 'https://github.com/owner/repo/pull/42',
state: 'open',
draft: false,
head: { ref: 'feature/test', sha: 'abc' },
base: { ref: 'main' },
user: { login: 'cascade-impl' },
},
repository: { full_name: 'owner/repo', html_url: 'https://github.com/owner/repo' },
sender: { login: 'cascade-impl' },
},
};

const result = await trigger.handle(ctx);
expect(result).not.toBeNull();
expect(result?.agentType).toBe('respond-to-review');
});

it('fires with undefined workItemId for null PR body', async () => {
const ctx: TriggerContext = {
project: mockProjectWithPrOpenedEnabled,
Expand Down