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
17 changes: 0 additions & 17 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,10 @@ export interface EnvConfig {
sentryDsn?: string;
}

function getEnvOrThrow(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}

function getEnvOrDefault(key: string, defaultValue: string): string {
return process.env[key] || defaultValue;
}

export function loadEnvConfig(): EnvConfig {
return {
port: Number.parseInt(getEnvOrDefault('PORT', '3000'), 10),
logLevel: getEnvOrDefault('LOG_LEVEL', 'info'),
databaseUrl: getEnvOrThrow('DATABASE_URL'),
sentryDsn: process.env.SENTRY_DSN,
};
}

export function loadEnvConfigSafe(): Omit<EnvConfig, 'databaseUrl'> & { databaseUrl?: string } {
return {
port: Number.parseInt(getEnvOrDefault('PORT', '3000'), 10),
Expand Down
62 changes: 0 additions & 62 deletions src/github/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,6 @@ export interface CreatedPR {
title: string;
}

export type GitHubReactionContent =
| '+1'
| '-1'
| 'laugh'
| 'confused'
| 'heart'
| 'hooray'
| 'rocket'
| 'eyes';

export const githubClient = {
async getPR(owner: string, repo: string, prNumber: number): Promise<PRDetails> {
logger.debug('Fetching PR', { owner, repo, prNumber });
Expand Down Expand Up @@ -423,36 +413,6 @@ export const githubClient = {
};
},

async addIssueCommentReaction(
owner: string,
repo: string,
commentId: number,
content: GitHubReactionContent,
): Promise<void> {
logger.debug('Adding reaction to issue comment', { owner, repo, commentId, content });
await getClient().reactions.createForIssueComment({
owner,
repo,
comment_id: commentId,
content,
});
},

async addReviewCommentReaction(
owner: string,
repo: string,
commentId: number,
content: GitHubReactionContent,
): Promise<void> {
logger.debug('Adding reaction to review comment', { owner, repo, commentId, content });
await getClient().reactions.createForPullRequestReviewComment({
owner,
repo,
comment_id: commentId,
content,
});
},

async getFailedWorkflowRunJobs(
owner: string,
repo: string,
Expand Down Expand Up @@ -513,23 +473,6 @@ export const githubClient = {
};
},

async branchExists(owner: string, repo: string, branch: string): Promise<boolean> {
logger.debug('Checking if branch exists', { owner, repo, branch });
try {
await getClient().repos.getBranch({
owner,
repo,
branch,
});
return true;
} catch (error) {
if (error instanceof Error && 'status' in error && error.status === 404) {
return false;
}
throw error;
}
},

async mergePR(
owner: string,
repo: string,
Expand All @@ -546,11 +489,6 @@ export const githubClient = {
},
};

export async function getAuthenticatedUser(): Promise<string> {
const { data } = await getClient().users.getAuthenticated();
return data.login;
}

export async function getGitHubUserForToken(token: string | null): Promise<string | null> {
if (!token) return null;

Expand Down
2 changes: 1 addition & 1 deletion src/pm/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function hasAutoLabel(
* Extract a human-readable PR title from a GitHub PR URL.
* E.g. "https://github.com/owner/repo/pull/123" → "Pull Request #123"
*/
export function extractPRTitle(prUrl: string): string {
function extractPRTitle(prUrl: string): string {
const match = prUrl.match(/\/pull\/(\d+)/);
return match ? `Pull Request #${match[1]}` : 'Pull Request';
}
Expand Down
2 changes: 1 addition & 1 deletion src/trello/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function withTrelloCredentials<T>(
return trelloCredentialStore.run(creds, fn);
}

export function getTrelloCredentials(): TrelloCredentials {
function getTrelloCredentials(): TrelloCredentials {
const scoped = trelloCredentialStore.getStore();
if (!scoped) {
throw new Error(
Expand Down
2 changes: 0 additions & 2 deletions src/trello/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export type { TrelloCard, TrelloComment, TrelloAttachment } from './client.js';

export interface TrelloCredentials {
apiKey: string;
token: string;
Expand Down
60 changes: 0 additions & 60 deletions src/triggers/shared/backlog-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,63 +184,3 @@ async function checkJiraCapacity(

return { atCapacity: false, reason: 'below-capacity', inFlightCount, limit };
}

// ---------------------------------------------------------------------------
// isBacklogEmpty (deprecated)
// ---------------------------------------------------------------------------

/**
* @deprecated Use `isPipelineAtCapacity` instead.
*
* Returns `true` when the project's backlog list/queue is empty.
*
* Supports Trello and JIRA. For any other provider type, or when required
* config fields are missing, returns `false` (conservative: let the agent run).
*
* @param project - Resolved project configuration
* @param provider - An initialised PM provider instance
*/
export async function isBacklogEmpty(
project: ProjectConfig,
provider: PMProvider,
): Promise<boolean> {
try {
if (provider.type === 'trello') {
const backlogListId = getTrelloConfig(project)?.lists?.backlog;
if (!backlogListId) {
logger.warn('isBacklogEmpty: no backlog list configured for Trello project', {
projectId: project.id,
});
return false;
}
const items = await provider.listWorkItems(backlogListId);
return items.length === 0;
}

if (provider.type === 'jira') {
const jiraConfig = getJiraConfig(project);
const backlogStatus = jiraConfig?.statuses?.backlog;
const projectKey = jiraConfig?.projectKey;
if (!backlogStatus || !projectKey) {
logger.warn('isBacklogEmpty: no backlog status or projectKey configured for JIRA project', {
projectId: project.id,
});
return false;
}
const items = await provider.listWorkItems(projectKey, { status: backlogStatus });
return items.length === 0;
}

logger.warn('isBacklogEmpty: unsupported PM provider type', {
providerType: provider.type,
projectId: project.id,
});
return false;
} catch (err) {
logger.warn('isBacklogEmpty: failed to check backlog, assuming non-empty', {
projectId: project.id,
error: String(err),
});
return false;
}
}
3 changes: 0 additions & 3 deletions tests/helpers/sharedMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,7 @@ export const mockGithubClient = {
createPRReview: vi.fn(),
getOpenPRByBranch: vi.fn(),
createPR: vi.fn(),
addIssueCommentReaction: vi.fn(),
addReviewCommentReaction: vi.fn(),
getFailedWorkflowRunJobs: vi.fn(),
branchExists: vi.fn(),
mergePR: vi.fn(),
} satisfies GitHubClientContract;

Expand Down
112 changes: 0 additions & 112 deletions tests/unit/github/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,6 @@ const mockActions = {
listJobsForWorkflowRun: vi.fn(),
};

const mockReactions = {
createForIssueComment: vi.fn(),
createForPullRequestReviewComment: vi.fn(),
};

const mockRepos = {
getBranch: vi.fn(),
};

const mockUsers = {
getAuthenticated: vi.fn(),
};
Expand All @@ -47,8 +38,6 @@ vi.mock('@octokit/rest', () => ({
issues: mockIssues,
checks: mockChecks,
actions: mockActions,
reactions: mockReactions,
repos: mockRepos,
users: mockUsers,
})),
}));
Expand All @@ -63,7 +52,6 @@ vi.mock('../../../src/utils/logging.js', () => ({
}));

import {
getAuthenticatedUser,
getGitHubUserForToken,
githubClient,
withGitHubToken,
Expand Down Expand Up @@ -646,50 +634,6 @@ describe('githubClient', () => {
});
});

describe('branchExists', () => {
it('returns true when branch exists', async () => {
mockRepos.getBranch.mockResolvedValue({ data: {} });

const result = await withGitHubToken('test-token', () =>
githubClient.branchExists('owner', 'repo', 'main'),
);

expect(result).toBe(true);
});

it('returns false when branch does not exist (404)', async () => {
const error = new Error('Not Found') as Error & { status: number };
error.status = 404;
mockRepos.getBranch.mockRejectedValue(error);

const result = await withGitHubToken('test-token', () =>
githubClient.branchExists('owner', 'repo', 'nonexistent'),
);

expect(result).toBe(false);
});

it('throws on other errors', async () => {
mockRepos.getBranch.mockRejectedValue(new Error('Server Error'));

await expect(
withGitHubToken('test-token', () => githubClient.branchExists('owner', 'repo', 'branch')),
).rejects.toThrow('Server Error');
});
});

describe('getAuthenticatedUser', () => {
it('returns authenticated user login', async () => {
mockUsers.getAuthenticated.mockResolvedValue({
data: { login: 'cascade-bot' },
});

const result = await withGitHubToken('test-token', () => getAuthenticatedUser());

expect(result).toBe('cascade-bot');
});
});

describe('withGitHubToken', () => {
it('scopes a different Octokit instance within the callback', async () => {
mockPulls.get.mockResolvedValue({
Expand All @@ -715,62 +659,6 @@ describe('githubClient', () => {
});
});

describe('addIssueCommentReaction', () => {
it('calls reactions.createForIssueComment with correct params', async () => {
mockReactions.createForIssueComment.mockResolvedValue({ data: {} });

await withGitHubToken('test-token', () =>
githubClient.addIssueCommentReaction('owner', 'repo', 42, 'eyes'),
);

expect(mockReactions.createForIssueComment).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
comment_id: 42,
content: 'eyes',
});
});

it('propagates errors from the API', async () => {
mockReactions.createForIssueComment.mockRejectedValue(new Error('403 Forbidden'));

await expect(
withGitHubToken('test-token', () =>
githubClient.addIssueCommentReaction('owner', 'repo', 42, 'eyes'),
),
).rejects.toThrow('403 Forbidden');
});
});

describe('addReviewCommentReaction', () => {
it('calls reactions.createForPullRequestReviewComment with correct params', async () => {
mockReactions.createForPullRequestReviewComment.mockResolvedValue({ data: {} });

await withGitHubToken('test-token', () =>
githubClient.addReviewCommentReaction('owner', 'repo', 99, 'heart'),
);

expect(mockReactions.createForPullRequestReviewComment).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
comment_id: 99,
content: 'heart',
});
});

it('propagates errors from the API', async () => {
mockReactions.createForPullRequestReviewComment.mockRejectedValue(
new Error('422 Unprocessable'),
);

await expect(
withGitHubToken('test-token', () =>
githubClient.addReviewCommentReaction('owner', 'repo', 99, 'eyes'),
),
).rejects.toThrow('422 Unprocessable');
});
});

describe('getGitHubUserForToken', () => {
it('returns null when token is null', async () => {
const result = await getGitHubUserForToken(null);
Expand Down
21 changes: 0 additions & 21 deletions tests/unit/pm/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,12 @@ import '../../../src/pm/index.js';
import {
PMLifecycleManager,
type ProjectPMConfig,
extractPRTitle,
resolveProjectPMConfig,
} from '../../../src/pm/lifecycle.js';
import type { PMProvider } from '../../../src/pm/types.js';
import type { ProjectConfig } from '../../../src/types/index.js';

describe('pm/lifecycle', () => {
describe('extractPRTitle', () => {
it('extracts PR number from a standard GitHub PR URL', () => {
expect(extractPRTitle('https://github.com/owner/repo/pull/123')).toBe('Pull Request #123');
});

it('extracts PR number from a PR URL with trailing path', () => {
expect(extractPRTitle('https://github.com/owner/repo/pull/42/files')).toBe(
'Pull Request #42',
);
});

it('returns generic title when URL does not contain /pull/', () => {
expect(extractPRTitle('https://example.com/no-pull-here')).toBe('Pull Request');
});

it('returns generic title for empty string', () => {
expect(extractPRTitle('')).toBe('Pull Request');
});
});

describe('resolveProjectPMConfig', () => {
it('returns JIRA config when project type is jira', () => {
const project: ProjectConfig = {
Expand Down
Loading
Loading