refactor: implement discriminated unions for GitHub contexts (#360)

* feat: add agent mode for automation scenarios

- Add agent mode that always triggers without checking for mentions
- Implement Mode interface with support for mode-specific tool configuration
- Add getAllowedTools() and getDisallowedTools() methods to Mode interface
- Simplify tests by combining related test cases
- Update documentation and examples to include agent mode
- Fix TypeScript imports to prevent circular dependencies

Agent mode is designed for automation and workflow_dispatch scenarios
where Claude should always run without requiring trigger phrases.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Minor update to readme (from @main to @beta)

* Since workflow_dispatch isn't in the base action, update the examples accordingly

* minor formatting issue

* Update to say beta instead of main

* Fix missed tracking comment to be false

* add schedule & workflow dispatch paths. Also make prepare logic conditional

* tests

* Add test workflow for workflow_dispatch functionality

* Update workflow to use correct branch reference

* remove test workflow dispatch file

* minor lint update

* update workflow dispatch agent example

* minor lint update

* refactor: simplify prepare logic with mode-specific implementations

* ensure tag mode can't work with workflow dispatch and schedule tasks

* simplify: remove workflow_dispatch/schedule from create-prompt

- Remove workflow_dispatch and schedule event handling from create-prompt
  since agent mode doesn't use the standard prompt generation flow
- Enforce mode compatibility at selection time in the registry instead
  of runtime validation in tag mode
- Add explanatory comment in agent mode about why prompt file is needed
- Update tests to reflect simplified event handling

This reduces code duplication and makes the separation between tag mode
(entity-based events) and agent mode (automation events) clearer.

* simplify PR by making agent mode only work with workflow dispatch and schedule events

* remove unnecessary changes

* remove unnecessary changes from PR

- Revert update-comment-link.ts changes (agent mode doesn't use this)
- Revert create-initial.ts changes (agent mode doesn't create comments)
- Remove unused default-branch.ts file
- Revert install-mcp-server.ts changes (agent mode uses minimal MCP)

These files are only used by tag mode for entity-based events, not needed
for workflow_dispatch/schedule support via agent mode.

* fix: handle optional entityNumber for TypeScript

- Add runtime checks in files that require entityNumber
- These files are only used by tag mode which always has entityNumber
- Agent mode (workflow_dispatch/schedule) doesn't use these files

* linting update

* refactor: implement discriminated unions for GitHub contexts

Split ParsedGitHubContext into entity-specific and automation contexts:
- ParsedGitHubContext: For entity events (issues/PRs) with required entityNumber and isPR
- AutomationContext: For workflow_dispatch/schedule events without entity fields
- GitHubContext: Union type for all contexts

This eliminates ~20 null checks throughout the codebase and provides better type safety.
Entity-specific code paths are now guaranteed to have the required fields.

Co-Authored-By: Claude <noreply@anthropic.com>

* update comment

* More robust type checking

* refactor: improve discriminated union implementation based on review feedback

- Use eventName checks instead of 'in' operator for more robust type guards
- Remove unnecessary type assertions - TypeScript's control flow analysis works correctly
- Remove redundant runtime checks for entityNumber and isPR
- Simplify code by using context directly after type guard

Co-Authored-By: Claude <noreply@anthropic.com>

* some structural simplification

* refactor: further simplify discriminated union implementation

- Add event name constants to reduce duplication
- Derive EntityEventName and AutomationEventName types from constants
- Use isAutomationContext consistently in agent mode and registry
- Simplify parseGitHubContext by removing redundant type assertions
- Extract payload casts to variables for cleaner code

Co-Authored-By: Claude <noreply@anthropic.com>

* bun format

* specify the type

* minor linting update again

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
km-anthropic
2025-07-29 14:58:59 -07:00
committed by GitHub
parent bdfdd1f788
commit daac7e353f
15 changed files with 166 additions and 133 deletions

View File

@@ -125,10 +125,8 @@ export function prepareContext(
const isPR = context.isPR; const isPR = context.isPR;
// Get PR/Issue number from entityNumber // Get PR/Issue number from entityNumber
const prNumber = const prNumber = isPR ? context.entityNumber.toString() : undefined;
isPR && context.entityNumber ? context.entityNumber.toString() : undefined; const issueNumber = !isPR ? context.entityNumber.toString() : undefined;
const issueNumber =
!isPR && context.entityNumber ? context.entityNumber.toString() : undefined;
// Extract trigger username and comment data based on event type // Extract trigger username and comment data based on event type
let triggerUsername: string | undefined; let triggerUsername: string | undefined;

View File

@@ -9,7 +9,7 @@ import * as core from "@actions/core";
import { setupGitHubToken } from "../github/token"; import { setupGitHubToken } from "../github/token";
import { checkWritePermissions } from "../github/validation/permissions"; import { checkWritePermissions } from "../github/validation/permissions";
import { createOctokit } from "../github/api/client"; import { createOctokit } from "../github/api/client";
import { parseGitHubContext } from "../github/context"; import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry"; import { getMode } from "../modes/registry";
import { prepare } from "../prepare"; import { prepare } from "../prepare";
@@ -22,7 +22,8 @@ async function run() {
// Step 2: Parse GitHub context (once for all operations) // Step 2: Parse GitHub context (once for all operations)
const context = parseGitHubContext(); const context = parseGitHubContext();
// Step 3: Check write permissions // Step 3: Check write permissions (only for entity contexts)
if (isEntityContext(context)) {
const hasWritePermissions = await checkWritePermissions( const hasWritePermissions = await checkWritePermissions(
octokit.rest, octokit.rest,
context, context,
@@ -32,6 +33,7 @@ async function run() {
"Actor does not have write permissions to the repository", "Actor does not have write permissions to the repository",
); );
} }
}
// Step 4: Get mode and check trigger conditions // Step 4: Get mode and check trigger conditions
const mode = getMode(context.inputs.mode, context); const mode = getMode(context.inputs.mode, context);

View File

@@ -9,6 +9,7 @@ import {
import { import {
parseGitHubContext, parseGitHubContext,
isPullRequestReviewCommentEvent, isPullRequestReviewCommentEvent,
isEntityContext,
} from "../github/context"; } from "../github/context";
import { GITHUB_SERVER_URL } from "../github/api/config"; import { GITHUB_SERVER_URL } from "../github/api/config";
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup"; import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
@@ -23,13 +24,13 @@ async function run() {
const triggerUsername = process.env.TRIGGER_USERNAME; const triggerUsername = process.env.TRIGGER_USERNAME;
const context = parseGitHubContext(); const context = parseGitHubContext();
const { owner, repo } = context.repository;
// This script is only called for entity-based events // This script is only called for entity-based events
if (!context.entityNumber) { if (!isEntityContext(context)) {
throw new Error("update-comment-link requires an entity number"); throw new Error("update-comment-link requires an entity context");
} }
const entityNumber = context.entityNumber;
const { owner, repo } = context.repository;
const octokit = createOctokit(githubToken); const octokit = createOctokit(githubToken);
@@ -80,7 +81,7 @@ async function run() {
const { data: pr } = await octokit.rest.pulls.get({ const { data: pr } = await octokit.rest.pulls.get({
owner, owner,
repo, repo,
pull_number: entityNumber, pull_number: context.entityNumber,
}); });
console.log(`PR state: ${pr.state}`); console.log(`PR state: ${pr.state}`);
console.log(`PR comments count: ${pr.comments}`); console.log(`PR comments count: ${pr.comments}`);

View File

@@ -37,9 +37,24 @@ export type ScheduleEvent = {
import type { ModeName } from "../modes/types"; import type { ModeName } from "../modes/types";
import { DEFAULT_MODE, isValidMode } from "../modes/registry"; import { DEFAULT_MODE, isValidMode } from "../modes/registry";
export type ParsedGitHubContext = { // Event name constants for better maintainability
const ENTITY_EVENT_NAMES = [
"issues",
"issue_comment",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
] as const;
const AUTOMATION_EVENT_NAMES = ["workflow_dispatch", "schedule"] as const;
// Derive types from constants for better maintainability
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
type AutomationEventName = (typeof AUTOMATION_EVENT_NAMES)[number];
// Common fields shared by all context types
type BaseContext = {
runId: string; runId: string;
eventName: string;
eventAction?: string; eventAction?: string;
repository: { repository: {
owner: string; owner: string;
@@ -47,16 +62,6 @@ export type ParsedGitHubContext = {
full_name: string; full_name: string;
}; };
actor: string; actor: string;
payload:
| IssuesEvent
| IssueCommentEvent
| PullRequestEvent
| PullRequestReviewEvent
| PullRequestReviewCommentEvent
| WorkflowDispatchEvent
| ScheduleEvent;
entityNumber?: number;
isPR?: boolean;
inputs: { inputs: {
mode: ModeName; mode: ModeName;
triggerPhrase: string; triggerPhrase: string;
@@ -75,7 +80,29 @@ export type ParsedGitHubContext = {
}; };
}; };
export function parseGitHubContext(): ParsedGitHubContext { // Context for entity-based events (issues, PRs, comments)
export type ParsedGitHubContext = BaseContext & {
eventName: EntityEventName;
payload:
| IssuesEvent
| IssueCommentEvent
| PullRequestEvent
| PullRequestReviewEvent
| PullRequestReviewCommentEvent;
entityNumber: number;
isPR: boolean;
};
// Context for automation events (workflow_dispatch, schedule)
export type AutomationContext = BaseContext & {
eventName: AutomationEventName;
payload: WorkflowDispatchEvent | ScheduleEvent;
};
// Union type for all contexts
export type GitHubContext = ParsedGitHubContext | AutomationContext;
export function parseGitHubContext(): GitHubContext {
const context = github.context; const context = github.context;
const modeInput = process.env.MODE ?? DEFAULT_MODE; const modeInput = process.env.MODE ?? DEFAULT_MODE;
@@ -85,7 +112,6 @@ export function parseGitHubContext(): ParsedGitHubContext {
const commonFields = { const commonFields = {
runId: process.env.GITHUB_RUN_ID!, runId: process.env.GITHUB_RUN_ID!,
eventName: context.eventName,
eventAction: context.payload.action, eventAction: context.payload.action,
repository: { repository: {
owner: context.repo.owner, owner: context.repo.owner,
@@ -115,61 +141,67 @@ export function parseGitHubContext(): ParsedGitHubContext {
switch (context.eventName) { switch (context.eventName) {
case "issues": { case "issues": {
const payload = context.payload as IssuesEvent;
return { return {
...commonFields, ...commonFields,
payload: context.payload as IssuesEvent, eventName: "issues",
entityNumber: (context.payload as IssuesEvent).issue.number, payload,
entityNumber: payload.issue.number,
isPR: false, isPR: false,
}; };
} }
case "issue_comment": { case "issue_comment": {
const payload = context.payload as IssueCommentEvent;
return { return {
...commonFields, ...commonFields,
payload: context.payload as IssueCommentEvent, eventName: "issue_comment",
entityNumber: (context.payload as IssueCommentEvent).issue.number, payload,
isPR: Boolean( entityNumber: payload.issue.number,
(context.payload as IssueCommentEvent).issue.pull_request, isPR: Boolean(payload.issue.pull_request),
),
}; };
} }
case "pull_request": { case "pull_request": {
const payload = context.payload as PullRequestEvent;
return { return {
...commonFields, ...commonFields,
payload: context.payload as PullRequestEvent, eventName: "pull_request",
entityNumber: (context.payload as PullRequestEvent).pull_request.number, payload,
entityNumber: payload.pull_request.number,
isPR: true, isPR: true,
}; };
} }
case "pull_request_review": { case "pull_request_review": {
const payload = context.payload as PullRequestReviewEvent;
return { return {
...commonFields, ...commonFields,
payload: context.payload as PullRequestReviewEvent, eventName: "pull_request_review",
entityNumber: (context.payload as PullRequestReviewEvent).pull_request payload,
.number, entityNumber: payload.pull_request.number,
isPR: true, isPR: true,
}; };
} }
case "pull_request_review_comment": { case "pull_request_review_comment": {
const payload = context.payload as PullRequestReviewCommentEvent;
return { return {
...commonFields, ...commonFields,
payload: context.payload as PullRequestReviewCommentEvent, eventName: "pull_request_review_comment",
entityNumber: (context.payload as PullRequestReviewCommentEvent) payload,
.pull_request.number, entityNumber: payload.pull_request.number,
isPR: true, isPR: true,
}; };
} }
case "workflow_dispatch": { case "workflow_dispatch": {
return { return {
...commonFields, ...commonFields,
eventName: "workflow_dispatch",
payload: context.payload as unknown as WorkflowDispatchEvent, payload: context.payload as unknown as WorkflowDispatchEvent,
// No entityNumber or isPR for workflow_dispatch
}; };
} }
case "schedule": { case "schedule": {
return { return {
...commonFields, ...commonFields,
eventName: "schedule",
payload: context.payload as unknown as ScheduleEvent, payload: context.payload as unknown as ScheduleEvent,
// No entityNumber or isPR for schedule
}; };
} }
default: default:
@@ -205,37 +237,53 @@ export function parseAdditionalPermissions(s: string): Map<string, string> {
} }
export function isIssuesEvent( export function isIssuesEvent(
context: ParsedGitHubContext, context: GitHubContext,
): context is ParsedGitHubContext & { payload: IssuesEvent } { ): context is ParsedGitHubContext & { payload: IssuesEvent } {
return context.eventName === "issues"; return context.eventName === "issues";
} }
export function isIssueCommentEvent( export function isIssueCommentEvent(
context: ParsedGitHubContext, context: GitHubContext,
): context is ParsedGitHubContext & { payload: IssueCommentEvent } { ): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
return context.eventName === "issue_comment"; return context.eventName === "issue_comment";
} }
export function isPullRequestEvent( export function isPullRequestEvent(
context: ParsedGitHubContext, context: GitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestEvent } { ): context is ParsedGitHubContext & { payload: PullRequestEvent } {
return context.eventName === "pull_request"; return context.eventName === "pull_request";
} }
export function isPullRequestReviewEvent( export function isPullRequestReviewEvent(
context: ParsedGitHubContext, context: GitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } { ): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
return context.eventName === "pull_request_review"; return context.eventName === "pull_request_review";
} }
export function isPullRequestReviewCommentEvent( export function isPullRequestReviewCommentEvent(
context: ParsedGitHubContext, context: GitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } { ): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
return context.eventName === "pull_request_review_comment"; return context.eventName === "pull_request_review_comment";
} }
export function isIssuesAssignedEvent( export function isIssuesAssignedEvent(
context: ParsedGitHubContext, context: GitHubContext,
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } { ): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
return isIssuesEvent(context) && context.eventAction === "assigned"; return isIssuesEvent(context) && context.eventAction === "assigned";
} }
// Type guard to check if context is an entity context (has entityNumber and isPR)
export function isEntityContext(
context: GitHubContext,
): context is ParsedGitHubContext {
return ENTITY_EVENT_NAMES.includes(context.eventName as EntityEventName);
}
// Type guard to check if context is an automation context
export function isAutomationContext(
context: GitHubContext,
): context is AutomationContext {
return AUTOMATION_EVENT_NAMES.includes(
context.eventName as AutomationEventName,
);
}

View File

@@ -22,12 +22,6 @@ export async function createInitialComment(
) { ) {
const { owner, repo } = context.repository; const { owner, repo } = context.repository;
// This function is only called for entity-based events
if (!context.entityNumber) {
throw new Error("createInitialComment requires an entity number");
}
const entityNumber = context.entityNumber;
const jobRunLink = createJobRunLink(owner, repo, context.runId); const jobRunLink = createJobRunLink(owner, repo, context.runId);
const initialBody = createCommentBody(jobRunLink); const initialBody = createCommentBody(jobRunLink);
@@ -42,7 +36,7 @@ export async function createInitialComment(
const comments = await octokit.rest.issues.listComments({ const comments = await octokit.rest.issues.listComments({
owner, owner,
repo, repo,
issue_number: entityNumber, issue_number: context.entityNumber,
}); });
const existingComment = comments.data.find((comment) => { const existingComment = comments.data.find((comment) => {
const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID; const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID;
@@ -65,7 +59,7 @@ export async function createInitialComment(
response = await octokit.rest.issues.createComment({ response = await octokit.rest.issues.createComment({
owner, owner,
repo, repo,
issue_number: entityNumber, issue_number: context.entityNumber,
body: initialBody, body: initialBody,
}); });
} }
@@ -74,7 +68,7 @@ export async function createInitialComment(
response = await octokit.rest.pulls.createReplyForReviewComment({ response = await octokit.rest.pulls.createReplyForReviewComment({
owner, owner,
repo, repo,
pull_number: entityNumber, pull_number: context.entityNumber,
comment_id: context.payload.comment.id, comment_id: context.payload.comment.id,
body: initialBody, body: initialBody,
}); });
@@ -83,7 +77,7 @@ export async function createInitialComment(
response = await octokit.rest.issues.createComment({ response = await octokit.rest.issues.createComment({
owner, owner,
repo, repo,
issue_number: entityNumber, issue_number: context.entityNumber,
body: initialBody, body: initialBody,
}); });
} }
@@ -101,7 +95,7 @@ export async function createInitialComment(
const response = await octokit.rest.issues.createComment({ const response = await octokit.rest.issues.createComment({
owner, owner,
repo, repo,
issue_number: entityNumber, issue_number: context.entityNumber,
body: initialBody, body: initialBody,
}); });

View File

@@ -1,6 +1,7 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { mkdir, writeFile } from "fs/promises"; import { mkdir, writeFile } from "fs/promises";
import type { Mode, ModeOptions, ModeResult } from "../types"; import type { Mode, ModeOptions, ModeResult } from "../types";
import { isAutomationContext } from "../../github/context";
/** /**
* Agent mode implementation. * Agent mode implementation.
@@ -15,10 +16,7 @@ export const agentMode: Mode = {
shouldTrigger(context) { shouldTrigger(context) {
// Only trigger for automation events // Only trigger for automation events
return ( return isAutomationContext(context);
context.eventName === "workflow_dispatch" ||
context.eventName === "schedule"
);
}, },
prepareContext(context) { prepareContext(context) {

View File

@@ -13,7 +13,8 @@
import type { Mode, ModeName } from "./types"; import type { Mode, ModeName } from "./types";
import { tagMode } from "./tag"; import { tagMode } from "./tag";
import { agentMode } from "./agent"; import { agentMode } from "./agent";
import type { ParsedGitHubContext } from "../github/context"; import type { GitHubContext } from "../github/context";
import { isAutomationContext } from "../github/context";
export const DEFAULT_MODE = "tag" as const; export const DEFAULT_MODE = "tag" as const;
export const VALID_MODES = ["tag", "agent"] as const; export const VALID_MODES = ["tag", "agent"] as const;
@@ -34,7 +35,7 @@ const modes = {
* @returns The requested mode * @returns The requested mode
* @throws Error if the mode is not found or cannot handle the event * @throws Error if the mode is not found or cannot handle the event
*/ */
export function getMode(name: ModeName, context: ParsedGitHubContext): Mode { export function getMode(name: ModeName, context: GitHubContext): Mode {
const mode = modes[name]; const mode = modes[name];
if (!mode) { if (!mode) {
const validModes = VALID_MODES.join("', '"); const validModes = VALID_MODES.join("', '");
@@ -44,11 +45,7 @@ export function getMode(name: ModeName, context: ParsedGitHubContext): Mode {
} }
// Validate mode can handle the event type // Validate mode can handle the event type
if ( if (name === "tag" && isAutomationContext(context)) {
name === "tag" &&
(context.eventName === "workflow_dispatch" ||
context.eventName === "schedule")
) {
throw new Error( throw new Error(
`Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`, `Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`,
); );

View File

@@ -8,6 +8,7 @@ import { configureGitAuth } from "../../github/operations/git-config";
import { prepareMcpConfig } from "../../mcp/install-mcp-server"; import { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { fetchGitHubData } from "../../github/data/fetcher"; import { fetchGitHubData } from "../../github/data/fetcher";
import { createPrompt } from "../../create-prompt"; import { createPrompt } from "../../create-prompt";
import { isEntityContext } from "../../github/context";
/** /**
* Tag mode implementation. * Tag mode implementation.
@@ -21,6 +22,10 @@ export const tagMode: Mode = {
description: "Traditional implementation mode triggered by @claude mentions", description: "Traditional implementation mode triggered by @claude mentions",
shouldTrigger(context) { shouldTrigger(context) {
// Tag mode only handles entity events
if (!isEntityContext(context)) {
return false;
}
return checkContainsTrigger(context); return checkContainsTrigger(context);
}, },
@@ -51,7 +56,10 @@ export const tagMode: Mode = {
octokit, octokit,
githubToken, githubToken,
}: ModeOptions): Promise<ModeResult> { }: ModeOptions): Promise<ModeResult> {
// Tag mode handles entity-based events (issues, PRs, comments) // Tag mode only handles entity-based events
if (!isEntityContext(context)) {
throw new Error("Tag mode requires entity context");
}
// Check if actor is human // Check if actor is human
await checkHumanActor(octokit.rest, context); await checkHumanActor(octokit.rest, context);
@@ -60,11 +68,6 @@ export const tagMode: Mode = {
const commentData = await createInitialComment(octokit.rest, context); const commentData = await createInitialComment(octokit.rest, context);
const commentId = commentData.id; const commentId = commentData.id;
// Fetch GitHub data - entity events always have entityNumber and isPR
if (!context.entityNumber || context.isPR === undefined) {
throw new Error("Entity events must have entityNumber and isPR defined");
}
const githubData = await fetchGitHubData({ const githubData = await fetchGitHubData({
octokits: octokit, octokits: octokit,
repository: `${context.repository.owner}/${context.repository.repo}`, repository: `${context.repository.owner}/${context.repository.repo}`,

View File

@@ -1,10 +1,10 @@
import type { ParsedGitHubContext } from "../github/context"; import type { GitHubContext } from "../github/context";
export type ModeName = "tag" | "agent"; export type ModeName = "tag" | "agent";
export type ModeContext = { export type ModeContext = {
mode: ModeName; mode: ModeName;
githubContext: ParsedGitHubContext; githubContext: GitHubContext;
commentId?: number; commentId?: number;
baseBranch?: string; baseBranch?: string;
claudeBranch?: string; claudeBranch?: string;
@@ -32,12 +32,12 @@ export type Mode = {
/** /**
* Determines if this mode should trigger based on the GitHub context * Determines if this mode should trigger based on the GitHub context
*/ */
shouldTrigger(context: ParsedGitHubContext): boolean; shouldTrigger(context: GitHubContext): boolean;
/** /**
* Prepares the mode context with any additional data needed for prompt generation * Prepares the mode context with any additional data needed for prompt generation
*/ */
prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext; prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
/** /**
* Returns the list of tools that should be allowed for this mode * Returns the list of tools that should be allowed for this mode
@@ -64,7 +64,7 @@ export type Mode = {
// Define types for mode prepare method to avoid circular dependencies // Define types for mode prepare method to avoid circular dependencies
export type ModeOptions = { export type ModeOptions = {
context: ParsedGitHubContext; context: GitHubContext;
octokit: any; // We'll use any to avoid circular dependency with Octokits octokit: any; // We'll use any to avoid circular dependency with Octokits
githubToken: string; githubToken: string;
}; };

View File

@@ -1,4 +1,4 @@
import type { ParsedGitHubContext } from "../github/context"; import type { GitHubContext } from "../github/context";
import type { Octokits } from "../github/api/client"; import type { Octokits } from "../github/api/client";
import type { Mode } from "../modes/types"; import type { Mode } from "../modes/types";
@@ -13,7 +13,7 @@ export type PrepareResult = {
}; };
export type PrepareOptions = { export type PrepareOptions = {
context: ParsedGitHubContext; context: GitHubContext;
octokit: Octokits; octokit: Octokits;
mode: Mode; mode: Mode;
githubToken: string; githubToken: string;

View File

@@ -1,4 +1,7 @@
import type { ParsedGitHubContext } from "../src/github/context"; import type {
ParsedGitHubContext,
AutomationContext,
} from "../src/github/context";
import type { import type {
IssuesEvent, IssuesEvent,
IssueCommentEvent, IssueCommentEvent,
@@ -38,7 +41,7 @@ export const createMockContext = (
): ParsedGitHubContext => { ): ParsedGitHubContext => {
const baseContext: ParsedGitHubContext = { const baseContext: ParsedGitHubContext = {
runId: "1234567890", runId: "1234567890",
eventName: "", eventName: "issue_comment", // Default to a valid entity event
eventAction: "", eventAction: "",
repository: defaultRepository, repository: defaultRepository,
actor: "test-actor", actor: "test-actor",
@@ -55,6 +58,22 @@ export const createMockContext = (
return { ...baseContext, ...overrides }; return { ...baseContext, ...overrides };
}; };
export const createMockAutomationContext = (
overrides: Partial<AutomationContext> = {},
): AutomationContext => {
const baseContext: AutomationContext = {
runId: "1234567890",
eventName: "workflow_dispatch",
eventAction: undefined,
repository: defaultRepository,
actor: "test-actor",
payload: {} as any,
inputs: defaultInputs,
};
return { ...baseContext, ...overrides };
};
export const mockIssueOpenedContext: ParsedGitHubContext = { export const mockIssueOpenedContext: ParsedGitHubContext = {
runId: "1234567890", runId: "1234567890",
eventName: "issues", eventName: "issues",

View File

@@ -1,15 +1,14 @@
import { describe, test, expect, beforeEach } from "bun:test"; import { describe, test, expect, beforeEach } from "bun:test";
import { agentMode } from "../../src/modes/agent"; import { agentMode } from "../../src/modes/agent";
import type { ParsedGitHubContext } from "../../src/github/context"; import type { GitHubContext } from "../../src/github/context";
import { createMockContext } from "../mockContext"; import { createMockContext, createMockAutomationContext } from "../mockContext";
describe("Agent Mode", () => { describe("Agent Mode", () => {
let mockContext: ParsedGitHubContext; let mockContext: GitHubContext;
beforeEach(() => { beforeEach(() => {
mockContext = createMockContext({ mockContext = createMockAutomationContext({
eventName: "workflow_dispatch", eventName: "workflow_dispatch",
isPR: false,
}); });
}); });
@@ -34,30 +33,26 @@ describe("Agent Mode", () => {
test("agent mode only triggers for workflow_dispatch and schedule events", () => { test("agent mode only triggers for workflow_dispatch and schedule events", () => {
// Should trigger for automation events // Should trigger for automation events
const workflowDispatchContext = createMockContext({ const workflowDispatchContext = createMockAutomationContext({
eventName: "workflow_dispatch", eventName: "workflow_dispatch",
isPR: false,
}); });
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true); expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true);
const scheduleContext = createMockContext({ const scheduleContext = createMockAutomationContext({
eventName: "schedule", eventName: "schedule",
isPR: false,
}); });
expect(agentMode.shouldTrigger(scheduleContext)).toBe(true); expect(agentMode.shouldTrigger(scheduleContext)).toBe(true);
// Should NOT trigger for other events // Should NOT trigger for entity events
const otherEvents = [ const entityEvents = [
"push",
"repository_dispatch",
"issue_comment", "issue_comment",
"pull_request", "pull_request",
"pull_request_review", "pull_request_review",
"issues", "issues",
]; ] as const;
otherEvents.forEach((eventName) => { entityEvents.forEach((eventName) => {
const context = createMockContext({ eventName, isPR: false }); const context = createMockContext({ eventName });
expect(agentMode.shouldTrigger(context)).toBe(false); expect(agentMode.shouldTrigger(context)).toBe(false);
}); });
}); });

View File

@@ -3,18 +3,18 @@ import { getMode, isValidMode } from "../../src/modes/registry";
import type { ModeName } from "../../src/modes/types"; import type { ModeName } from "../../src/modes/types";
import { tagMode } from "../../src/modes/tag"; import { tagMode } from "../../src/modes/tag";
import { agentMode } from "../../src/modes/agent"; import { agentMode } from "../../src/modes/agent";
import { createMockContext } from "../mockContext"; import { createMockContext, createMockAutomationContext } from "../mockContext";
describe("Mode Registry", () => { describe("Mode Registry", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
eventName: "issue_comment", eventName: "issue_comment",
}); });
const mockWorkflowDispatchContext = createMockContext({ const mockWorkflowDispatchContext = createMockAutomationContext({
eventName: "workflow_dispatch", eventName: "workflow_dispatch",
}); });
const mockScheduleContext = createMockContext({ const mockScheduleContext = createMockAutomationContext({
eventName: "schedule", eventName: "schedule",
}); });

View File

@@ -299,15 +299,4 @@ describe("parseEnvVarsWithContext", () => {
expect(result.allowedTools).toBe("Tool1,Tool2"); expect(result.allowedTools).toBe("Tool1,Tool2");
}); });
}); });
test("should throw error for unsupported event type", () => {
process.env = BASE_ENV;
const unsupportedContext = createMockContext({
eventName: "unsupported_event",
eventAction: "whatever",
});
expect(() => prepareContext(unsupportedContext, "12345")).toThrow(
"Unsupported event type: unsupported_event",
);
});
}); });

View File

@@ -474,17 +474,6 @@ describe("checkContainsTrigger", () => {
}); });
}); });
}); });
describe("non-matching events", () => {
it("should return false for non-matching event type", () => {
const context = createMockContext({
eventName: "push",
eventAction: "created",
payload: {} as any,
});
expect(checkContainsTrigger(context)).toBe(false);
});
});
}); });
describe("escapeRegExp", () => { describe("escapeRegExp", () => {