diff --git a/src/commands/auth/token/handler.test.ts b/src/commands/auth/token/handler.test.ts new file mode 100644 index 0000000..f9b3859 --- /dev/null +++ b/src/commands/auth/token/handler.test.ts @@ -0,0 +1,96 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../../../src/errors.js'; +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { mockAuthTokenMintRequest } from '../../../../test/auth/fixtures.js'; +import { withNock } from '../../../../test/support/http-test.js'; + +test('runOrfeCore mints an auth token for the resolved caller bot', async () => { + await withNock(async () => { + const api = mockAuthTokenMintRequest({ repo: { owner: 'throw-if-null', name: 'orfe' } }); + + const result = await runCoreCommand({ + command: 'auth token', + input: { repo: 'throw-if-null/orfe' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'auth token', + repo: 'throw-if-null/orfe', + data: { + bot: 'greg', + app_slug: 'GR3G-BOT', + repo: 'throw-if-null/orfe', + token: 'ghs_123', + expires_at: '2026-04-06T12:00:00Z', + auth_mode: 'github-app', + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool resolves auth token from context.agent and returns shared success envelope', async () => { + await withNock(async () => { + const api = mockAuthTokenMintRequest(); + + const result = await runToolCommand({ + input: { + command: 'auth token', + repo: 'throw-if-null/orfe', + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'auth token', + repo: 'throw-if-null/orfe', + data: { + bot: 'greg', + app_slug: 'GR3G-BOT', + repo: 'throw-if-null/orfe', + token: 'ghs_123', + expires_at: '2026-04-06T12:00:00Z', + auth_mode: 'github-app', + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore rejects bot override input for auth token', async () => { + await assert.rejects( + runCoreCommand({ + command: 'auth token', + input: { bot: 'unknown', repo: 'throw-if-null/orfe' }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'invalid_usage'); + assert.equal(error.message, 'Command "auth token" does not accept input field "bot".'); + return true; + }, + ); +}); + +test('executeOrfeTool rejects bot override input for auth token', async () => { + const result = await runToolCommand({ + input: { + command: 'auth token', + bot: 'greg', + repo: 'throw-if-null/orfe', + }, + }); + + assert.deepEqual(result, { + ok: false, + command: 'auth token', + error: { + code: 'invalid_usage', + message: 'Command "auth token" does not accept input field "bot".', + retryable: false, + }, + }); +}); diff --git a/src/commands/issue/comment/handler.test.ts b/src/commands/issue/comment/handler.test.ts new file mode 100644 index 0000000..e2ef47e --- /dev/null +++ b/src/commands/issue/comment/handler.test.ts @@ -0,0 +1,127 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockIssueCommentRequest } from '../../../../test/issue/fixtures.js'; + +test('runOrfeCore posts a generic issue comment and returns structured success output', async () => { + await withNock(async () => { + const api = mockIssueCommentRequest({ issueNumber: 14, body: 'Hello from orfe' }); + + const result = await runCoreCommand({ + command: 'issue comment', + input: { issue_number: 14, body: 'Hello from orfe' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue comment', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + comment_id: 123456, + html_url: 'https://github.com/throw-if-null/orfe/issues/14#issuecomment-123456', + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for issue comment', async () => { + await withNock(async () => { + const api = mockIssueCommentRequest({ issueNumber: 14, body: 'Hello from orfe' }); + + const result = await runToolCommand({ + input: { command: 'issue comment', issue_number: 14, body: 'Hello from orfe' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue comment', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + comment_id: 123456, + html_url: 'https://github.com/throw-if-null/orfe/issues/14#issuecomment-123456', + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue comment not-found responses clearly', async () => { + await withNock(async () => { + const api = mockIssueCommentRequest({ + issueNumber: 999, + body: 'Hello from orfe', + issueGetStatus: 404, + issueGetResponseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue comment', + input: { issue_number: 999, body: 'Hello from orfe' }, + }), + /Issue #999 was not found\./, + ); + + assert.equal(api.isDone(), false); + }); +}); + +test('runOrfeCore maps issue comment auth failures clearly', async () => { + await withNock(async () => { + const api = mockIssueCommentRequest({ + issueNumber: 14, + body: 'Hello from orfe', + issueGetStatus: 403, + issueGetResponseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue comment', + input: { issue_number: 14, body: 'Hello from orfe' }, + }), + /GitHub App authentication failed while commenting on issue #14\./, + ); + + assert.equal(api.isDone(), false); + }); +}); + +test('runOrfeCore rejects pull request targets for issue comment clearly', async () => { + await withNock(async () => { + const api = mockIssueCommentRequest({ + issueNumber: 46, + body: 'Hello from orfe', + issueGetResponseBody: { + number: 46, + title: 'Implement `orfe issue comment`', + body: 'PR body', + state: 'open', + state_reason: null, + labels: [], + assignees: [], + html_url: 'https://github.com/throw-if-null/orfe/pull/46', + pull_request: { + url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/46', + }, + }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue comment', + input: { issue_number: 46, body: 'Hello from orfe' }, + }), + /Issue #46 is a pull request\. Use pr comment instead\./, + ); + + assert.equal(api.isDone(), false); + }); +}); diff --git a/src/commands/issue/create/handler.test.ts b/src/commands/issue/create/handler.test.ts new file mode 100644 index 0000000..25a7e80 --- /dev/null +++ b/src/commands/issue/create/handler.test.ts @@ -0,0 +1,940 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../../../src/errors.js'; +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockIssueCreateRequest } from '../../../../test/issue/fixtures.js'; +import { + createProjectFieldsConnection, + createProjectItemNode, + createProjectItemsConnection, + createProjectLookupResponse, + createProjectStatusFieldNode, + createProjectStatusValueNode, + mockProjectAddItemRequest, + mockProjectGetStatusRequest, + mockProjectLookupRequest, + mockProjectStatusFieldsRequest, + mockProjectStatusUpdateRequest, +} from '../../../../test/project/fixtures.js'; +import { + createRepoConfigWithDefaultProject, + renderIssueBodyContractMarker, +} from '../../../../test/support/runtime-fixtures.js'; + +test('runOrfeCore creates a generic issue and returns structured success output', async () => { + await withNock(async () => { + const api = mockIssueCreateRequest({ + requestBody: { + title: 'New issue title', + body: 'Body text', + labels: ['needs-input'], + assignees: ['greg'], + }, + }); + + const result = await runCoreCommand({ + command: 'issue create', + input: { + title: 'New issue title', + body: 'Body text', + labels: ['needs-input'], + assignees: ['greg'], + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for issue create', async () => { + await withNock(async () => { + const api = mockIssueCreateRequest({ + requestBody: { + title: 'New issue title', + body: 'Body text', + labels: ['needs-input'], + assignees: ['greg'], + }, + }); + + const result = await runToolCommand({ + input: { + command: 'issue create', + title: 'New issue title', + body: 'Body text', + labels: ['needs-input'], + assignees: ['greg'], + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore validates issue-create bodies against explicit contracts and appends provenance', async () => { + await withNock(async () => { + const issueBody = [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Agent-authored issues validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: add new durable docs', + '', + '## ADR needed?', + '', + '- ADR needed: yes', + ].join('\n'); + + const api = mockIssueCreateRequest({ + requestBody: { + title: 'New issue title', + body: `${issueBody}\n\n${renderIssueBodyContractMarker()}`, + }, + }); + + const result = await runCoreCommand({ + command: 'issue create', + input: { + title: 'New issue title', + body: issueBody, + body_contract: 'formal-work-item@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool validates issue bodies through body contracts before create', async () => { + await withNock(async () => { + const issueBody = [ + '## Problem / context', + '', + 'Need deterministic validation for issue bodies.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against declarative contracts.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: add new durable docs', + '', + '## ADR needed?', + '', + '- ADR needed: yes', + ].join('\n'); + + const api = mockIssueCreateRequest({ + requestBody: { + title: 'New issue title', + body: `${issueBody}\n\n${renderIssueBodyContractMarker()}`, + }, + }); + + const result = await runToolCommand({ + input: { + command: 'issue create', + title: 'New issue title', + body: issueBody, + body_contract: 'formal-work-item@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore can create an issue and add it to the default project when explicitly requested', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + projectItemId: 'PVTI_lAHOABCD1234', + includeAuth: false, + }); + + const result = await runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', add_to_project: true }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + project_assignment: { + project_owner: 'throw-if-null', + project_number: 1, + project_item_id: 'PVTI_lAHOABCD1234', + }, + }, + }); + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('executeOrfeTool returns project assignment details for issue create when explicitly requested', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const api = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + graphqlResponseBody: createProjectLookupResponse({ projectId: 'PVT_project_1', ownerType: 'organization' }), + includeAuth: false, + }); + const addApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + projectItemId: 'PVTI_lAHOABCD1234', + includeAuth: false, + }); + + const result = await runToolCommand({ + input: { command: 'issue create', title: 'New issue title', add_to_project: true }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + project_assignment: { + project_owner: 'throw-if-null', + project_number: 1, + project_item_id: 'PVTI_lAHOABCD1234', + }, + }, + }); + assert.equal(issueApi.isDone(), true); + assert.equal(api.isDone(), true); + assert.equal(addApi.isDone(), true); + }); +}); + +test('runOrfeCore can create an issue and add it to a user-owned project when explicitly requested', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'octocat', + projectNumber: 7, + projectId: 'PVT_project_user_7', + graphqlResponseBody: createProjectLookupResponse({ projectId: 'PVT_project_user_7', ownerType: 'user' }), + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_user_7', + contentId: 'I_kwDOOrfeIssue21', + projectItemId: 'PVTI_lAHOABCDUSER', + includeAuth: false, + }); + + const result = await runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', add_to_project: true, project_owner: 'octocat', project_number: 7 }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + project_assignment: { + project_owner: 'octocat', + project_number: 7, + project_item_id: 'PVTI_lAHOABCDUSER', + }, + }, + }); + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('runOrfeCore can create an issue and add it to an organization-owned project when explicitly requested', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_org_1', + graphqlResponseBody: createProjectLookupResponse({ projectId: 'PVT_project_org_1', ownerType: 'organization' }), + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_org_1', + contentId: 'I_kwDOOrfeIssue21', + projectItemId: 'PVTI_lAHOABCDORG', + includeAuth: false, + }); + + const result = await runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', add_to_project: true }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + project_assignment: { + project_owner: 'throw-if-null', + project_number: 1, + project_item_id: 'PVTI_lAHOABCDORG', + }, + }, + }); + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('executeOrfeTool returns project assignment details for issue create with a user-owned project', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'octocat', + projectNumber: 7, + projectId: 'PVT_project_user_7', + graphqlResponseBody: createProjectLookupResponse({ projectId: 'PVT_project_user_7', ownerType: 'user' }), + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_user_7', + contentId: 'I_kwDOOrfeIssue21', + projectItemId: 'PVTI_lAHOABCDUSER', + includeAuth: false, + }); + + const result = await runToolCommand({ + input: { + command: 'issue create', + title: 'New issue title', + add_to_project: true, + project_owner: 'octocat', + project_number: 7, + }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + project_assignment: { + project_owner: 'octocat', + project_number: 7, + project_item_id: 'PVTI_lAHOABCDUSER', + }, + }, + }); + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('runOrfeCore can create an issue, add it to a project, and set initial status', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + projectItemId: 'PVTI_lAHOABCD1234', + includeAuth: false, + }); + const currentItemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 21, + includeAuth: false, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + const mutationApi = mockProjectStatusUpdateRequest({ + projectId: 'PVT_project_1', + itemId: 'PVTI_lAHOABCD1234', + fieldId: 'PVTSSF_lAHOABCD1234', + optionId: 'f75ad845', + }); + const observedItemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 21, + includeAuth: false, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ + fieldId: 'PVTSSF_lAHOABCD1234', + fieldName: 'Status', + optionId: 'f75ad845', + name: 'Todo', + }), + }), + ]), + }, + }, + }, + }, + }); + const observedFieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', status: 'Todo' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + project_assignment: { + project_owner: 'throw-if-null', + project_number: 1, + project_item_id: 'PVTI_lAHOABCD1234', + status_field_name: 'Status', + status_option_id: 'f75ad845', + status: 'Todo', + }, + }, + }); + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + assert.equal(currentItemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + assert.equal(mutationApi.isDone(), true); + assert.equal(observedItemApi.isDone(), true); + assert.equal(observedFieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore preserves create-only behavior when repo config has a default project but no project assignment was requested', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + + const result = await runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue create', + repo: 'throw-if-null/orfe', + data: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + }); + assert.equal(issueApi.isDone(), true); + }); +}); + +test('runOrfeCore surfaces partial failure details when issue create succeeds but project add fails', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + graphqlStatus: 403, + graphqlResponseBody: { message: 'Resource not accessible by integration' }, + includeAuth: false, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', add_to_project: true }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'auth_failed'); + assert.equal( + error.message, + 'Issue #21 was created, but adding it to GitHub Project throw-if-null/1 failed: GitHub App authentication failed while adding issue #21 to a GitHub Project.', + ); + assert.deepEqual(error.details, { + stage: 'project_add', + created_issue: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + project_owner: 'throw-if-null', + project_number: 1, + }); + return true; + }, + ); + + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('executeOrfeTool returns partial-failure details for issue create project assignment errors', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + graphqlStatus: 403, + graphqlResponseBody: { message: 'Resource not accessible by integration' }, + includeAuth: false, + }); + + const result = await runToolCommand({ + input: { command: 'issue create', title: 'New issue title', add_to_project: true }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: false, + command: 'issue create', + error: { + code: 'auth_failed', + message: + 'Issue #21 was created, but adding it to GitHub Project throw-if-null/1 failed: GitHub App authentication failed while adding issue #21 to a GitHub Project.', + retryable: false, + details: { + stage: 'project_add', + created_issue: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + project_owner: 'throw-if-null', + project_number: 1, + }, + }, + }); + + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('runOrfeCore reports project_add when status was requested but project add fails', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + graphqlStatus: 403, + graphqlResponseBody: { message: 'Resource not accessible by integration' }, + includeAuth: false, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', status: 'Todo' }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'auth_failed'); + assert.deepEqual(error.details, { + stage: 'project_add', + created_issue: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + project_owner: 'throw-if-null', + project_number: 1, + status_field_name: 'Status', + requested_status: 'Todo', + }); + return true; + }, + ); + + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('executeOrfeTool returns project_add partial-failure details when status was requested but project add failed', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + graphqlStatus: 403, + graphqlResponseBody: { message: 'Resource not accessible by integration' }, + includeAuth: false, + }); + + const result = await runToolCommand({ + input: { command: 'issue create', title: 'New issue title', status: 'Todo' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: false, + command: 'issue create', + error: { + code: 'auth_failed', + message: + 'Issue #21 was created, but adding it to GitHub Project throw-if-null/1 failed: GitHub App authentication failed while adding issue #21 to a GitHub Project.', + retryable: false, + details: { + stage: 'project_add', + created_issue: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + project_owner: 'throw-if-null', + project_number: 1, + status_field_name: 'Status', + requested_status: 'Todo', + }, + }, + }); + + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + }); +}); + +test('runOrfeCore surfaces partial failure details when issue create succeeds but project status fails', async () => { + await withNock(async () => { + const issueApi = mockIssueCreateRequest({ requestBody: { title: 'New issue title' } }); + const projectLookupApi = mockProjectLookupRequest({ + projectOwner: 'throw-if-null', + projectNumber: 1, + projectId: 'PVT_project_1', + includeAuth: false, + }); + const projectAddApi = mockProjectAddItemRequest({ + projectId: 'PVT_project_1', + contentId: 'I_kwDOOrfeIssue21', + projectItemId: 'PVTI_lAHOABCD1234', + includeAuth: false, + }); + const currentItemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 21, + includeAuth: false, + graphqlStatus: 403, + graphqlResponseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', status: 'Todo' }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'auth_failed'); + assert.equal( + error.message, + 'Issue #21 was created and added to GitHub Project throw-if-null/1, but setting initial status failed: GitHub App authentication failed while setting project status for issue #21.', + ); + assert.deepEqual(error.details, { + stage: 'project_status', + created_issue: { + issue_number: 21, + title: 'New issue title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/21', + created: true, + }, + project_owner: 'throw-if-null', + project_number: 1, + status_field_name: 'Status', + requested_status: 'Todo', + }); + return true; + }, + ); + + assert.equal(issueApi.isDone(), true); + assert.equal(projectLookupApi.isDone(), true); + assert.equal(projectAddApi.isDone(), true); + assert.equal(currentItemApi.isDone(), true); + }); +}); + +test('runOrfeCore maps issue create auth failures clearly', async () => { + await withNock(async () => { + const api = mockIssueCreateRequest({ + requestBody: { title: 'New issue title' }, + status: 403, + responseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title' }, + }), + /GitHub App authentication failed while creating an issue in throw-if-null\/orfe\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue create missing repository failures clearly', async () => { + await withNock(async () => { + const api = mockIssueCreateRequest({ + repo: { owner: 'octo', name: 'missing' }, + requestBody: { title: 'New issue title' }, + status: 404, + responseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title', repo: 'octo/missing' }, + }), + /Repository octo\/missing was not found\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue create creation failures clearly', async () => { + await withNock(async () => { + const api = mockIssueCreateRequest({ + requestBody: { title: 'New issue title' }, + status: 422, + responseBody: { message: 'Validation Failed' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue create', + input: { title: 'New issue title' }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'internal_error'); + assert.equal(error.message, 'GitHub issue creation failed with status 422: Validation Failed'); + assert.equal(error.retryable, false); + return true; + }, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/issue/get/handler.test.ts b/src/commands/issue/get/handler.test.ts new file mode 100644 index 0000000..5a8f9fd --- /dev/null +++ b/src/commands/issue/get/handler.test.ts @@ -0,0 +1,104 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockIssueGetRequest } from '../../../../test/issue/fixtures.js'; + +test('runOrfeCore reads an issue and returns structured success output', async () => { + await withNock(async () => { + const api = mockIssueGetRequest({ issueNumber: 14 }); + + const result = await runCoreCommand({ + command: 'issue get', + input: { issue_number: 14 }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue get', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + title: 'Build `orfe` foundation and runtime scaffolding', + body: 'Issue body', + state: 'open', + state_reason: null, + labels: ['needs-input'], + assignees: ['greg'], + html_url: 'https://github.com/throw-if-null/orfe/issues/14', + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for issue get', async () => { + await withNock(async () => { + const api = mockIssueGetRequest({ issueNumber: 14 }); + + const result = await runToolCommand({ + input: { + command: 'issue get', + issue_number: 14, + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue get', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + title: 'Build `orfe` foundation and runtime scaffolding', + body: 'Issue body', + state: 'open', + state_reason: null, + labels: ['needs-input'], + assignees: ['greg'], + html_url: 'https://github.com/throw-if-null/orfe/issues/14', + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue get not-found responses clearly', async () => { + await withNock(async () => { + const api = mockIssueGetRequest({ + issueNumber: 999, + status: 404, + responseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue get', + input: { issue_number: 999 }, + }), + /Issue #999 was not found\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue get auth failures clearly', async () => { + await withNock(async () => { + const api = mockIssueGetRequest({ + issueNumber: 14, + status: 403, + responseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue get', + input: { issue_number: 14 }, + }), + /GitHub App authentication failed while reading issue #14\./, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/issue/set-state/handler.test.ts b/src/commands/issue/set-state/handler.test.ts new file mode 100644 index 0000000..f4d6344 --- /dev/null +++ b/src/commands/issue/set-state/handler.test.ts @@ -0,0 +1,346 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { + createIssueRestResponse, + createIssueStateNode, + mockIssueSetStateDuplicateRequest, + mockIssueSetStateRequest, +} from '../../../../test/issue/fixtures.js'; + +test('runOrfeCore closes an issue with structured state metadata', async () => { + await withNock(async () => { + const api = mockIssueSetStateRequest({ + issueNumber: 14, + currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), + restUpdateBody: { state: 'closed', state_reason: 'completed' }, + observedIssueState: createIssueStateNode({ + id: 'I_14', + issueNumber: 14, + state: 'CLOSED', + stateReason: 'COMPLETED', + }), + }); + + const result = await runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'completed' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue set-state', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + state: 'closed', + state_reason: 'completed', + duplicate_of_issue_number: null, + changed: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore reopens an issue and clears duplicate metadata', async () => { + await withNock(async () => { + const api = mockIssueSetStateRequest({ + issueNumber: 14, + currentIssueState: createIssueStateNode({ + id: 'I_14', + issueNumber: 14, + state: 'CLOSED', + stateReason: 'DUPLICATE', + duplicateOfIssueNumber: 7, + duplicateOfId: 'I_7', + }), + unmark: { duplicateId: 'I_14', canonicalId: 'I_7' }, + restUpdateBody: { state: 'open' }, + observedIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), + }); + + const result = await runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'open' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue set-state', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + state: 'open', + state_reason: null, + duplicate_of_issue_number: null, + changed: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore closes an issue as a duplicate and returns canonical issue metadata', async () => { + await withNock(async () => { + const api = mockIssueSetStateDuplicateRequest({ + issueNumber: 14, + duplicateOfIssueNumber: 7, + currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), + canonicalIssueState: createIssueStateNode({ id: 'I_7', issueNumber: 7, state: 'OPEN' }), + mark: { duplicateId: 'I_14', canonicalId: 'I_7' }, + observedIssueState: createIssueStateNode({ + id: 'I_14', + issueNumber: 14, + state: 'CLOSED', + stateReason: 'DUPLICATE', + duplicateOfIssueNumber: 7, + duplicateOfId: 'I_7', + }), + }); + + const result = await runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 7 }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue set-state', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + state: 'closed', + state_reason: 'duplicate', + duplicate_of_issue_number: 7, + changed: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore treats matching issue set-state requests as no-ops', async () => { + await withNock(async () => { + const api = mockIssueSetStateRequest({ + issueNumber: 14, + currentIssueState: createIssueStateNode({ + id: 'I_14', + issueNumber: 14, + state: 'CLOSED', + stateReason: 'COMPLETED', + }), + includeGraphql: true, + }); + + const result = await runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'completed' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue set-state', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + state: 'closed', + state_reason: 'completed', + duplicate_of_issue_number: null, + changed: false, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue set-state missing duplicate target clearly', async () => { + await withNock(async () => { + const api = mockIssueSetStateDuplicateRequest({ + issueNumber: 14, + duplicateOfIssueNumber: 999, + currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), + canonicalIssueState: null, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 999 }, + }), + /Duplicate target issue #999 was not found\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore rejects pull request duplicate targets for issue set-state clearly', async () => { + await withNock(async () => { + const api = mockIssueSetStateDuplicateRequest({ + issueNumber: 14, + duplicateOfIssueNumber: 48, + currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), + canonicalIssueState: null, + duplicateTargetGetStatus: 200, + duplicateTargetGetResponseBody: createIssueRestResponse(48, { + title: 'Implement `orfe issue set-state`', + html_url: 'https://github.com/throw-if-null/orfe/pull/48', + pull_request: { + url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/48', + }, + }), + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 48 }, + }), + /Duplicate target issue #48 is a pull request\. --duplicate-of must reference an issue\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue set-state auth failures clearly', async () => { + await withNock(async () => { + const api = mockIssueSetStateRequest({ + issueNumber: 14, + currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), + issueGetStatus: 403, + issueGetResponseBody: { message: 'Resource not accessible by integration' }, + includeGraphql: false, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'completed' }, + }), + /GitHub App authentication failed while setting state for issue #14\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore rejects pull request targets for issue set-state clearly', async () => { + await withNock(async () => { + const api = mockIssueSetStateRequest({ + issueNumber: 46, + currentIssueState: createIssueStateNode({ id: 'I_46', issueNumber: 46, state: 'OPEN' }), + issueGetResponseBody: { + number: 46, + title: 'Implement `orfe issue set-state`', + body: 'PR body', + state: 'open', + state_reason: null, + labels: [], + assignees: [], + html_url: 'https://github.com/throw-if-null/orfe/pull/46', + pull_request: { + url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/46', + }, + }, + includeGraphql: false, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 46, state: 'closed', state_reason: 'completed' }, + }), + /Issue #46 is a pull request\. issue set-state only supports issues\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore re-targets duplicate issue set-state requests and reports changes', async () => { + await withNock(async () => { + const api = mockIssueSetStateDuplicateRequest({ + issueNumber: 14, + duplicateOfIssueNumber: 9, + currentIssueState: createIssueStateNode({ + id: 'I_14', + issueNumber: 14, + state: 'CLOSED', + stateReason: 'DUPLICATE', + duplicateOfIssueNumber: 7, + duplicateOfId: 'I_7', + }), + canonicalIssueState: createIssueStateNode({ id: 'I_9', issueNumber: 9, state: 'OPEN' }), + unmark: { duplicateId: 'I_14', canonicalId: 'I_7' }, + mark: { duplicateId: 'I_14', canonicalId: 'I_9' }, + observedIssueState: createIssueStateNode({ + id: 'I_14', + issueNumber: 14, + state: 'CLOSED', + stateReason: 'DUPLICATE', + duplicateOfIssueNumber: 9, + duplicateOfId: 'I_9', + }), + }); + + const result = await runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 9 }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue set-state', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + state: 'closed', + state_reason: 'duplicate', + duplicate_of_issue_number: 9, + changed: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore treats matching duplicate issue set-state requests as no-ops', async () => { + await withNock(async () => { + const api = mockIssueSetStateDuplicateRequest({ + issueNumber: 14, + duplicateOfIssueNumber: 7, + currentIssueState: createIssueStateNode({ + id: 'I_14', + issueNumber: 14, + state: 'CLOSED', + stateReason: 'DUPLICATE', + duplicateOfIssueNumber: 7, + duplicateOfId: 'I_7', + }), + canonicalIssueState: createIssueStateNode({ id: 'I_7', issueNumber: 7, state: 'OPEN' }), + }); + + const result = await runCoreCommand({ + command: 'issue set-state', + input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 7 }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue set-state', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + state: 'closed', + state_reason: 'duplicate', + duplicate_of_issue_number: 7, + changed: false, + }, + }); + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/issue/update/handler.test.ts b/src/commands/issue/update/handler.test.ts new file mode 100644 index 0000000..565fa29 --- /dev/null +++ b/src/commands/issue/update/handler.test.ts @@ -0,0 +1,254 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockIssueUpdateRequest } from '../../../../test/issue/fixtures.js'; +import { renderIssueBodyContractMarker } from '../../../../test/support/runtime-fixtures.js'; + +test('runOrfeCore updates issue metadata and returns structured success output', async () => { + await withNock(async () => { + const api = mockIssueUpdateRequest({ + issueNumber: 14, + requestBody: { + title: 'Updated title', + body: 'Updated body', + labels: ['bug', 'needs-input'], + assignees: ['greg'], + }, + }); + + const result = await runCoreCommand({ + command: 'issue update', + input: { + issue_number: 14, + title: 'Updated title', + body: 'Updated body', + labels: ['bug', 'needs-input'], + assignees: ['greg'], + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue update', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + title: 'Updated title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/14', + changed: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for issue update', async () => { + await withNock(async () => { + const api = mockIssueUpdateRequest({ + issueNumber: 14, + requestBody: { + title: 'Updated title', + labels: ['bug'], + }, + }); + + const result = await runToolCommand({ + input: { + command: 'issue update', + issue_number: 14, + title: 'Updated title', + labels: ['bug'], + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue update', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + title: 'Updated title', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/14', + changed: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore clears labels and assignees for issue update', async () => { + await withNock(async () => { + const api = mockIssueUpdateRequest({ + issueNumber: 14, + requestBody: { + labels: [], + assignees: [], + }, + responseBody: { + number: 14, + title: 'Build `orfe` foundation and runtime scaffolding', + body: 'Issue body', + state: 'open', + state_reason: null, + labels: [], + assignees: [], + html_url: 'https://github.com/throw-if-null/orfe/issues/14', + }, + }); + + const result = await runCoreCommand({ + command: 'issue update', + input: { + issue_number: 14, + clear_labels: true, + clear_assignees: true, + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue update', + repo: 'throw-if-null/orfe', + data: { + issue_number: 14, + title: 'Build `orfe` foundation and runtime scaffolding', + state: 'open', + html_url: 'https://github.com/throw-if-null/orfe/issues/14', + changed: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore allows provenance-only issue-update validation when no explicit contract is provided', async () => { + await withNock(async () => { + const issueBody = [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Agent-authored issues validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: add new durable docs', + '', + '## ADR needed?', + '', + '- ADR needed: yes', + '', + renderIssueBodyContractMarker(), + ].join('\n'); + + const api = mockIssueUpdateRequest({ + issueNumber: 14, + requestBody: { + body: issueBody, + }, + }); + + const result = await runCoreCommand({ + command: 'issue update', + input: { + issue_number: 14, + body: issueBody, + }, + }); + + assert.equal(result.ok, true); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue update not-found responses clearly', async () => { + await withNock(async () => { + const api = mockIssueUpdateRequest({ + issueNumber: 999, + requestBody: { title: 'Updated title' }, + status: 404, + responseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue update', + input: { issue_number: 999, title: 'Updated title' }, + }), + /Issue #999 was not found\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps issue update auth failures clearly', async () => { + await withNock(async () => { + const api = mockIssueUpdateRequest({ + issueNumber: 14, + requestBody: { title: 'Updated title' }, + status: 403, + responseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue update', + input: { issue_number: 14, title: 'Updated title' }, + }), + /GitHub App authentication failed while updating issue #14\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore rejects pull request targets for issue update clearly', async () => { + await withNock(async () => { + const api = mockIssueUpdateRequest({ + issueNumber: 46, + requestBody: { title: 'Updated title' }, + issueGetResponseBody: { + number: 46, + title: 'Implement `orfe issue update`', + body: 'PR body', + state: 'open', + state_reason: null, + labels: [], + assignees: [], + html_url: 'https://github.com/throw-if-null/orfe/pull/46', + pull_request: { + url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/46', + }, + }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'issue update', + input: { issue_number: 46, title: 'Updated title' }, + }), + /Issue #46 is a pull request\. issue update only supports issues\./, + ); + + assert.equal(api.isDone(), false); + }); +}); diff --git a/src/commands/issue/validate/handler.test.ts b/src/commands/issue/validate/handler.test.ts new file mode 100644 index 0000000..0e78557 --- /dev/null +++ b/src/commands/issue/validate/handler.test.ts @@ -0,0 +1,311 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; + +test('runOrfeCore can validate issue bodies without auth config or GitHub access', async () => { + const result = await runCoreCommand({ + command: 'issue validate', + input: { + body: [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: update existing docs', + '- Details: update docs/orfe/spec.md', + '', + '## ADR needed?', + '', + '- ADR needed: no', + '- Details: covered by ADR 0009', + '', + '## Dependencies / sequencing notes', + '', + '- depends on #59', + '', + '## Risks / open questions / non-goals', + '', + '- keep repo-specific structure out of runtime logic', + ].join('\n'), + body_contract: 'formal-work-item@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(result.command, 'issue validate'); + assert.equal(result.repo, 'throw-if-null/orfe'); + if (result.ok) { + assert.equal((result.data as { valid: boolean }).valid, true); + } +}); + +test('runOrfeCore returns structured issue validation results', async () => { + const result = await runCoreCommand({ + command: 'issue validate', + input: { + body: [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: update existing docs', + '- Details: update docs/orfe/spec.md', + '', + '## ADR needed?', + '', + '- ADR needed: no', + '- Details: covered by ADR 0009', + '', + '## Dependencies / sequencing notes', + '', + '- depends on #59', + '', + '## Risks / open questions / non-goals', + '', + '- keep repo-specific structure out of runtime logic', + ].join('\n'), + body_contract: 'formal-work-item@1.0.0', + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'issue validate', + repo: 'throw-if-null/orfe', + data: { + valid: true, + contract: { + artifact_type: 'issue', + contract_name: 'formal-work-item', + contract_version: '1.0.0', + }, + contract_source: 'explicit', + normalized_body: [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: update existing docs', + '- Details: update docs/orfe/spec.md', + '', + '## ADR needed?', + '', + '- ADR needed: no', + '- Details: covered by ADR 0009', + '', + '## Dependencies / sequencing notes', + '', + '- depends on #59', + '', + '## Risks / open questions / non-goals', + '', + '- keep repo-specific structure out of runtime logic', + '', + '', + ].join('\n'), + errors: [], + }, + }); +}); + +test('runOrfeCore returns structured issue validation results without GitHub access', async () => { + const result = await runCoreCommand({ + command: 'issue validate', + input: { + body: [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: update existing docs', + '- Details: update docs/orfe/spec.md', + '', + '## ADR needed?', + '', + '- ADR needed: no', + '- Details: covered by ADR 0009', + '', + '## Dependencies / sequencing notes', + '', + '- depends on #59', + '', + '## Risks / open questions / non-goals', + '', + '- keep repo-specific structure out of runtime logic', + ].join('\n'), + body_contract: 'formal-work-item@1.0.0', + }, + }); + + assert.equal(result.ok, true); + if (result.ok) { + assert.equal((result.data as { valid: boolean }).valid, true); + } +}); + +test('executeOrfeTool returns structured issue validation output', async () => { + const result = await runToolCommand({ + input: { + command: 'issue validate', + body: [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '### Out of scope', + '- executable plugins', + '', + '## Acceptance criteria', + '', + '- [ ] contracts load from .orfe/contracts', + '', + '## Docs impact', + '', + '- Docs impact: update existing docs', + '- Details: update docs/orfe/spec.md', + '', + '## ADR needed?', + '', + '- ADR needed: no', + '- Details: covered by ADR 0009', + '', + '## Dependencies / sequencing notes', + '', + '- depends on #59', + '', + '## Risks / open questions / non-goals', + '', + '- keep repo-specific structure out of runtime logic', + ].join('\n'), + body_contract: 'formal-work-item@1.0.0', + }, + }); + + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.command, 'issue validate'); + assert.equal((result.data as { valid: boolean }).valid, true); + } +}); + +test('runOrfeCore returns actionable issue validation failures', async () => { + const result = await runCoreCommand({ + command: 'issue validate', + input: { + body: [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against a versioned contract.', + '', + '## Scope', + '', + '### In scope', + '- declarative contracts', + '', + '## Docs impact', + '', + '- Docs impact: maybe', + '', + '## ADR needed?', + '', + '- ADR needed: no', + ].join('\n'), + body_contract: 'formal-work-item@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(result.command, 'issue validate'); + assert.equal(result.repo, 'throw-if-null/orfe'); + assert.equal((result.data as { valid: boolean }).valid, false); + assert.deepEqual( + (result.data as { errors: Array<{ kind: string }> }).errors.map((issue) => issue.kind), + ['missing_required_pattern', 'missing_required_section', 'invalid_allowed_value'], + ); +}); diff --git a/src/commands/pr/comment/handler.test.ts b/src/commands/pr/comment/handler.test.ts new file mode 100644 index 0000000..6df662e --- /dev/null +++ b/src/commands/pr/comment/handler.test.ts @@ -0,0 +1,143 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../../../src/errors.js'; +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockPullRequestCommentRequest } from '../../../../test/pr/fixtures.js'; + +test('runOrfeCore posts a top-level pull request comment and returns structured success output', async () => { + await withNock(async () => { + const api = mockPullRequestCommentRequest({ prNumber: 9, body: 'Hello from orfe' }); + + const result = await runCoreCommand({ + command: 'pr comment', + input: { pr_number: 9, body: 'Hello from orfe' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr comment', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + comment_id: 123456, + html_url: 'https://github.com/throw-if-null/orfe/pull/9#issuecomment-123456', + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for pr comment', async () => { + await withNock(async () => { + const api = mockPullRequestCommentRequest({ prNumber: 9, body: 'Hello from orfe' }); + + const result = await runToolCommand({ + input: { command: 'pr comment', pr_number: 9, body: 'Hello from orfe' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr comment', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + comment_id: 123456, + html_url: 'https://github.com/throw-if-null/orfe/pull/9#issuecomment-123456', + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr comment not-found responses clearly', async () => { + await withNock(async () => { + const api = mockPullRequestCommentRequest({ + prNumber: 999, + body: 'Hello from orfe', + status: 404, + responseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr comment', + input: { pr_number: 999, body: 'Hello from orfe' }, + }), + /Pull request #999 was not found\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps plain-issue targets for pr comment as not found', async () => { + await withNock(async () => { + const api = mockPullRequestCommentRequest({ + prNumber: 14, + body: 'Hello from orfe', + verifyStatus: 404, + verifyResponseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr comment', + input: { pr_number: 14, body: 'Hello from orfe' }, + }), + /Pull request #14 was not found\./, + ); + + assert.equal(api.isDone(), false); + }); +}); + +test('runOrfeCore maps pr comment auth failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestCommentRequest({ + prNumber: 9, + body: 'Hello from orfe', + status: 403, + responseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr comment', + input: { pr_number: 9, body: 'Hello from orfe' }, + }), + /GitHub App authentication failed while commenting on pull request #9\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore surfaces pr comment validation failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestCommentRequest({ + prNumber: 9, + body: 'Hello from orfe', + status: 422, + responseBody: { message: 'Validation Failed' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr comment', + input: { pr_number: 9, body: 'Hello from orfe' }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'internal_error'); + assert.equal(error.message, 'GitHub API request failed with status 422: Validation Failed'); + return true; + }, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/pr/get-or-create/handler.test.ts b/src/commands/pr/get-or-create/handler.test.ts new file mode 100644 index 0000000..befc4d6 --- /dev/null +++ b/src/commands/pr/get-or-create/handler.test.ts @@ -0,0 +1,411 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../../../src/errors.js'; +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockPullRequestGetOrCreateRequest } from '../../../../test/pr/fixtures.js'; +import { renderPrBodyContractMarker } from '../../../../test/support/runtime-fixtures.js'; + +test('runOrfeCore reuses an existing pull request for pr get-or-create', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + existingPullRequests: [ + { + number: 9, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + }, + ], + }); + + const result = await runCoreCommand({ + command: 'pr get-or-create', + input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr get-or-create', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + head: 'issues/orfe-13', + base: 'main', + draft: false, + created: false, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for pr get-or-create', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + existingPullRequests: [ + { + number: 9, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + }, + ], + }); + + const result = await runToolCommand({ + input: { command: 'pr get-or-create', head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr get-or-create', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + head: 'issues/orfe-13', + base: 'main', + draft: false, + created: false, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore reuses an existing pull request before validating unused body contract input', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + existingPullRequests: [ + { + number: 9, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + }, + ], + }); + + const result = await runCoreCommand({ + command: 'pr get-or-create', + input: { + head: 'issues/orfe-13', + title: 'Design the `orfe` custom tool and CLI contract', + body: 'Ref: #13\n\nCloses: #13', + body_contract: 'implementation-ready@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore creates a pull request for pr get-or-create when none exists', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + existingPullRequests: [], + createRequestBody: { + head: 'issues/orfe-13', + base: 'main', + title: 'Design the `orfe` custom tool and CLI contract', + body: 'Ref: #13', + draft: true, + }, + createResponseBody: { + number: 10, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'Ref: #13', + state: 'open', + draft: true, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/10', + }, + }); + + const result = await runCoreCommand({ + command: 'pr get-or-create', + input: { + head: 'issues/orfe-13', + title: 'Design the `orfe` custom tool and CLI contract', + body: 'Ref: #13', + draft: true, + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr get-or-create', + repo: 'throw-if-null/orfe', + data: { + pr_number: 10, + html_url: 'https://github.com/throw-if-null/orfe/pull/10', + head: 'issues/orfe-13', + base: 'main', + draft: true, + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool validates PR bodies through body contracts before create', async () => { + await withNock(async () => { + const prBody = [ + 'Ref: #59', + '', + '## Summary', + '', + '- add body-contract support', + '', + '## Verification', + '', + '- `npm test` ✅', + '- `npm run lint` ✅', + '- `npm run typecheck` ✅', + '- `npm run build` ✅', + '', + '## Docs / ADR / debt', + '', + '- docs updated: yes', + '- ADR updated: yes', + '- debt updated: yes', + '- details: updated docs and added ADR', + '', + '## Risks / follow-ups', + '', + '- richer generation is follow-up work', + ].join('\n'); + + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-59', + existingPullRequests: [], + createRequestBody: { + head: 'issues/orfe-59', + base: 'main', + title: 'Introduce versioned body-contract support', + body: `${prBody}\n\n${renderPrBodyContractMarker()}`, + draft: false, + }, + createResponseBody: { + number: 59, + title: 'Introduce versioned body-contract support', + body: `${prBody}\n\n${renderPrBodyContractMarker()}`, + state: 'open', + draft: false, + head: { ref: 'issues/orfe-59' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/59', + }, + }); + + const result = await runToolCommand({ + input: { + command: 'pr get-or-create', + head: 'issues/orfe-59', + title: 'Introduce versioned body-contract support', + body: prBody, + body_contract: 'implementation-ready@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore validates PR bodies against explicit contracts and appends provenance on create', async () => { + await withNock(async () => { + const prBody = [ + 'Ref: #59', + '', + '## Summary', + '', + '- add versioned body-contract support', + '', + '## Verification', + '', + '- `npm test` ✅', + '- `npm run lint` ✅', + '- `npm run typecheck` ✅', + '- `npm run build` ✅', + '', + '## Docs / ADR / debt', + '', + '- docs updated: yes', + '- ADR updated: yes', + '- debt updated: yes', + '- details: updated docs and added ADR', + '', + '## Risks / follow-ups', + '', + '- generation is still minimal in this slice', + ].join('\n'); + + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-59', + existingPullRequests: [], + createRequestBody: { + head: 'issues/orfe-59', + base: 'main', + title: 'Introduce versioned body-contract support', + body: `${prBody}\n\n${renderPrBodyContractMarker()}`, + draft: false, + }, + createResponseBody: { + number: 59, + title: 'Introduce versioned body-contract support', + body: `${prBody}\n\n${renderPrBodyContractMarker()}`, + state: 'open', + draft: false, + head: { ref: 'issues/orfe-59' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/59', + }, + }); + + const result = await runCoreCommand({ + command: 'pr get-or-create', + input: { + head: 'issues/orfe-59', + title: 'Introduce versioned body-contract support', + body: prBody, + body_contract: 'implementation-ready@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore rejects ambiguous pr get-or-create matches clearly', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + existingPullRequests: [ + { + number: 9, + title: 'First PR', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + }, + { + number: 10, + title: 'Second PR', + body: 'PR body', + state: 'open', + draft: true, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: 'https://github.com/throw-if-null/orfe/pull/10', + }, + ], + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr get-or-create', + input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'github_conflict'); + assert.equal( + error.message, + 'Found 2 open pull requests for head "issues/orfe-13" and base "main" in throw-if-null/orfe.', + ); + return true; + }, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr get-or-create lookup auth failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + listStatus: 403, + listResponseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr get-or-create', + input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, + }), + /GitHub App authentication failed while looking up pull requests for head "issues\/orfe-13" and base "main"\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr get-or-create creation auth failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + existingPullRequests: [], + createStatus: 403, + createResponseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr get-or-create', + input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, + }), + /GitHub App authentication failed while creating a pull request for head "issues\/orfe-13" and base "main"\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore surfaces pr get-or-create creation failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-13', + existingPullRequests: [], + createStatus: 422, + createResponseBody: { message: 'Validation Failed' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr get-or-create', + input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, + }), + /GitHub pull request creation failed with status 422: Validation Failed/, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/pr/get/handler.test.ts b/src/commands/pr/get/handler.test.ts new file mode 100644 index 0000000..760be02 --- /dev/null +++ b/src/commands/pr/get/handler.test.ts @@ -0,0 +1,101 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockPullRequestGetRequest } from '../../../../test/pr/fixtures.js'; + +test('runOrfeCore reads a pull request and returns structured success output', async () => { + await withNock(async () => { + const api = mockPullRequestGetRequest({ prNumber: 9 }); + + const result = await runCoreCommand({ + command: 'pr get', + input: { pr_number: 9 }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr get', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: 'issues/orfe-13', + base: 'main', + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for pr get', async () => { + await withNock(async () => { + const api = mockPullRequestGetRequest({ prNumber: 9 }); + + const result = await runToolCommand({ + input: { command: 'pr get', pr_number: 9 }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr get', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: 'issues/orfe-13', + base: 'main', + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr get not-found responses clearly', async () => { + await withNock(async () => { + const api = mockPullRequestGetRequest({ + prNumber: 999, + status: 404, + responseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr get', + input: { pr_number: 999 }, + }), + /Pull request #999 was not found\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr get auth failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestGetRequest({ + prNumber: 9, + status: 403, + responseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr get', + input: { pr_number: 9 }, + }), + /GitHub App authentication failed while reading pull request #9\./, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/pr/reply/handler.test.ts b/src/commands/pr/reply/handler.test.ts new file mode 100644 index 0000000..17f816a --- /dev/null +++ b/src/commands/pr/reply/handler.test.ts @@ -0,0 +1,150 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../../../src/errors.js'; +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockPullRequestReplyRequest } from '../../../../test/pr/fixtures.js'; + +test('runOrfeCore replies to a pull request review comment and returns structured success output', async () => { + await withNock(async () => { + const api = mockPullRequestReplyRequest({ prNumber: 9, commentId: 123456, body: 'ack' }); + + const result = await runCoreCommand({ + command: 'pr reply', + input: { pr_number: 9, comment_id: 123456, body: 'ack' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr reply', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + comment_id: 123999, + in_reply_to_comment_id: 123456, + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for pr reply', async () => { + await withNock(async () => { + const api = mockPullRequestReplyRequest({ prNumber: 9, commentId: 123456, body: 'ack' }); + + const result = await runToolCommand({ + input: { command: 'pr reply', pr_number: 9, comment_id: 123456, body: 'ack' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr reply', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + comment_id: 123999, + in_reply_to_comment_id: 123456, + created: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr reply missing pull requests clearly', async () => { + await withNock(async () => { + const api = mockPullRequestReplyRequest({ + prNumber: 404, + commentId: 123456, + body: 'ack', + verifyStatus: 404, + verifyResponseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr reply', + input: { pr_number: 404, comment_id: 123456, body: 'ack' }, + }), + /Pull request #404 was not found\./, + ); + + assert.equal(api.isDone(), false); + }); +}); + +test('runOrfeCore maps pr reply missing review comments clearly', async () => { + await withNock(async () => { + const api = mockPullRequestReplyRequest({ + prNumber: 9, + commentId: 123456, + body: 'ack', + status: 404, + responseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr reply', + input: { pr_number: 9, comment_id: 123456, body: 'ack' }, + }), + /Review comment #123456 was not found on pull request #9\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr reply auth failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestReplyRequest({ + prNumber: 9, + commentId: 123456, + body: 'ack', + status: 403, + responseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr reply', + input: { pr_number: 9, comment_id: 123456, body: 'ack' }, + }), + /GitHub App authentication failed while replying to review comment #123456 on pull request #9\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore rejects non-repliable pr reply targets clearly', async () => { + await withNock(async () => { + const api = mockPullRequestReplyRequest({ + prNumber: 9, + commentId: 123456, + body: 'ack', + status: 422, + responseBody: { message: 'Validation Failed' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr reply', + input: { pr_number: 9, comment_id: 123456, body: 'ack' }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'github_conflict'); + assert.equal( + error.message, + 'GitHub rejected reply to review comment #123456 on pull request #9: Validation Failed', + ); + return true; + }, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/pr/submit-review/handler.test.ts b/src/commands/pr/submit-review/handler.test.ts new file mode 100644 index 0000000..e563cc9 --- /dev/null +++ b/src/commands/pr/submit-review/handler.test.ts @@ -0,0 +1,141 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../../../src/errors.js'; +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { mockPullRequestSubmitReviewRequest } from '../../../../test/pr/fixtures.js'; + +test('runOrfeCore submits a pull request review and returns structured success output', async () => { + await withNock(async () => { + const api = mockPullRequestSubmitReviewRequest({ prNumber: 9, body: 'Looks good', event: 'APPROVE' }); + + const result = await runCoreCommand({ + command: 'pr submit-review', + input: { pr_number: 9, event: 'approve', body: 'Looks good' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr submit-review', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + review_id: 555, + event: 'approve', + submitted: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for pr submit-review', async () => { + await withNock(async () => { + const api = mockPullRequestSubmitReviewRequest({ prNumber: 9, body: 'Looks good', event: 'APPROVE' }); + + const result = await runToolCommand({ + input: { command: 'pr submit-review', pr_number: 9, event: 'approve', body: 'Looks good' }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr submit-review', + repo: 'throw-if-null/orfe', + data: { + pr_number: 9, + review_id: 555, + event: 'approve', + submitted: true, + }, + }); + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore rejects invalid pr submit-review events clearly', async () => { + await assert.rejects( + runCoreCommand({ + command: 'pr submit-review', + input: { pr_number: 9, event: 'dismiss', body: 'nope' }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'invalid_input'); + assert.equal(error.message, 'Review event must be one of: approve, request-changes, comment.'); + return true; + }, + ); +}); + +test('runOrfeCore maps pr submit-review missing pull requests clearly', async () => { + await withNock(async () => { + const api = mockPullRequestSubmitReviewRequest({ + prNumber: 404, + body: 'Looks good', + event: 'APPROVE', + verifyStatus: 404, + verifyResponseBody: { message: 'Not Found' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr submit-review', + input: { pr_number: 404, event: 'approve', body: 'Looks good' }, + }), + /Pull request #404 was not found\./, + ); + + assert.equal(api.isDone(), false); + }); +}); + +test('runOrfeCore maps pr submit-review auth failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestSubmitReviewRequest({ + prNumber: 9, + body: 'Looks good', + event: 'APPROVE', + status: 403, + responseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr submit-review', + input: { pr_number: 9, event: 'approve', body: 'Looks good' }, + }), + /GitHub App authentication failed while submitting a review on pull request #9\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore maps pr submit-review internal failures clearly', async () => { + await withNock(async () => { + const api = mockPullRequestSubmitReviewRequest({ + prNumber: 9, + body: 'Looks good', + event: 'APPROVE', + status: 422, + responseBody: { message: 'Validation Failed' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'pr submit-review', + input: { pr_number: 9, event: 'approve', body: 'Looks good' }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'internal_error'); + assert.equal(error.message, 'GitHub API request failed with status 422: Validation Failed'); + assert.equal(error.retryable, false); + return true; + }, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/pr/validate/handler.test.ts b/src/commands/pr/validate/handler.test.ts new file mode 100644 index 0000000..21e95e7 --- /dev/null +++ b/src/commands/pr/validate/handler.test.ts @@ -0,0 +1,200 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../../../src/errors.js'; +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; + +test('runOrfeCore returns structured PR validation results without GitHub access', async () => { + await withNock(async () => { + const result = await runCoreCommand({ + command: 'pr validate', + input: { + body: [ + 'Ref: #58', + '', + '## Summary', + '', + '- add PR body validation helpers', + '', + '## Verification', + '', + '- `npm test` ✅', + '- `npm run lint` ✅', + '- `npm run typecheck` ✅', + '- `npm run build` ✅', + '', + '## Docs / ADR / debt', + '', + '- docs updated: yes', + '- ADR updated: no', + '- debt updated: no', + '- details: updated docs/orfe/spec.md', + '', + '## Risks / follow-ups', + '', + '- none', + ].join('\n'), + body_contract: 'implementation-ready@1.0.0', + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr validate', + repo: 'throw-if-null/orfe', + data: { + valid: true, + contract: { + artifact_type: 'pr', + contract_name: 'implementation-ready', + contract_version: '1.0.0', + }, + contract_source: 'explicit', + normalized_body: [ + 'Ref: #58', + '', + '## Summary', + '', + '- add PR body validation helpers', + '', + '## Verification', + '', + '- `npm test` ✅', + '- `npm run lint` ✅', + '- `npm run typecheck` ✅', + '- `npm run build` ✅', + '', + '## Docs / ADR / debt', + '', + '- docs updated: yes', + '- ADR updated: no', + '- debt updated: no', + '- details: updated docs/orfe/spec.md', + '', + '## Risks / follow-ups', + '', + '- none', + '', + '', + ].join('\n'), + errors: [], + }, + }); + }); +}); + +test('executeOrfeTool returns structured PR validation output', async () => { + await withNock(async () => { + const result = await runToolCommand({ + input: { + command: 'pr validate', + body: 'Ref: #58\n\nCloses: #58', + body_contract: 'implementation-ready@1.0.0', + }, + }); + + assert.deepEqual(result, { + ok: true, + command: 'pr validate', + repo: 'throw-if-null/orfe', + data: { + valid: false, + contract: { + artifact_type: 'pr', + contract_name: 'implementation-ready', + contract_version: '1.0.0', + }, + contract_source: 'explicit', + errors: [ + { + kind: 'matched_forbidden_pattern', + scope: 'body', + pattern: '(?:^|\\n)(?:Closes|Close|Closed|Fixes|Fix|Fixed|Resolves|Resolve|Resolved)\\s*:?\\s*#\\d+', + message: + 'Body contract validation failed: body matched forbidden pattern (?:^|\\n)(?:Closes|Close|Closed|Fixes|Fix|Fixed|Resolves|Resolve|Resolved)\\s*:?\\s*#\\d+.', + }, + { + kind: 'missing_required_section', + scope: 'section', + section_heading: 'Summary', + message: 'Body contract validation failed: missing required section "Summary".', + }, + { + kind: 'missing_required_section', + scope: 'section', + section_heading: 'Verification', + message: 'Body contract validation failed: missing required section "Verification".', + }, + { + kind: 'missing_required_section', + scope: 'section', + section_heading: 'Docs / ADR / debt', + message: 'Body contract validation failed: missing required section "Docs / ADR / debt".', + }, + { + kind: 'missing_required_section', + scope: 'section', + section_heading: 'Risks / follow-ups', + message: 'Body contract validation failed: missing required section "Risks / follow-ups".', + }, + ], + }, + }); + }); +}); + +test('runOrfeCore returns actionable PR validation failures', async () => { + await withNock(async () => { + const result = await runCoreCommand({ + command: 'pr validate', + input: { + body: 'Ref: #58\n\nCloses: #58', + body_contract: 'implementation-ready@1.0.0', + }, + }); + + assert.equal(result.ok, true); + assert.equal(result.command, 'pr validate'); + assert.equal(result.repo, 'throw-if-null/orfe'); + assert.equal((result.data as { valid: boolean }).valid, false); + assert.deepEqual( + (result.data as { errors: Array<{ kind: string }> }).errors.map((issue) => issue.kind), + ['matched_forbidden_pattern', 'missing_required_section', 'missing_required_section', 'missing_required_section', 'missing_required_section'], + ); + }); +}); + +test('runOrfeCore fails clearly when contract validation fails', async () => { + await withNock(async () => { + const api = await import('../../../../test/pr/fixtures.js').then(({ mockPullRequestGetOrCreateRequest }) => + mockPullRequestGetOrCreateRequest({ + head: 'issues/orfe-59', + existingPullRequests: [], + }), + ); + + await assert.rejects( + runCoreCommand({ + command: 'pr get-or-create', + input: { + head: 'issues/orfe-59', + title: 'Introduce versioned body-contract support', + body: 'Ref: #59\n\nCloses: #59', + body_contract: 'implementation-ready@1.0.0', + }, + }), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'contract_validation_failed'); + assert.equal( + error.message, + 'Body contract validation failed: body matched forbidden pattern (?:^|\\n)(?:Closes|Close|Closed|Fixes|Fix|Fixed|Resolves|Resolve|Resolved)\\s*:?\\s*#\\d+.', + ); + return true; + }, + ); + + assert.equal(api.isDone(), true); + }); +}); diff --git a/src/commands/project/get-status/handler.test.ts b/src/commands/project/get-status/handler.test.ts new file mode 100644 index 0000000..cc10d57 --- /dev/null +++ b/src/commands/project/get-status/handler.test.ts @@ -0,0 +1,473 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { + createProjectFieldsConnection, + createProjectItemNode, + createProjectItemsConnection, + createProjectStatusFieldNode, + createProjectStatusValueNode, + mockProjectGetStatusRequest, + mockProjectStatusFieldsRequest, +} from '../../../../test/project/fixtures.js'; +import { createRepoConfigWithDefaultProject } from '../../../../test/support/runtime-fixtures.js'; + +test('runOrfeCore reads project status for an issue and returns structured success output', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + fields: [createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' })], + statusValue: createProjectStatusValueNode({ + fieldId: 'PVTSSF_lAHOABCD1234', + fieldName: 'Status', + optionId: 'f75ad846', + name: 'In Progress', + }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' })]), + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project get-status', + input: { item_type: 'issue', item_number: 13 }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'project get-status', + repo: 'throw-if-null/orfe', + data: { + project_owner: 'throw-if-null', + project_number: 1, + status_field_name: 'Status', + status_field_id: 'PVTSSF_lAHOABCD1234', + item_type: 'issue', + item_number: 13, + project_item_id: 'PVTI_lAHOABCD1234', + status_option_id: 'f75ad846', + status: 'In Progress', + }, + }); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for project get-status', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ + fieldId: 'PVTSSF_lAHOABCD1234', + fieldName: 'Status', + optionId: 'f75ad846', + name: 'In Progress', + }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' })]), + }, + }, + }, + }); + + const result = await runToolCommand({ + input: { command: 'project get-status', item_type: 'issue', item_number: 13 }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore paginates project items so later matching items are found', async () => { + await withNock(async () => { + const firstPageApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection( + [createProjectItemNode({ id: 'PVTI_elsewhere', projectId: 'PVT_elsewhere', projectOwner: 'throw-if-null', projectNumber: 99 })], + { hasNextPage: true, endCursor: 'cursor-1' }, + ), + }, + }, + }, + }, + }); + const secondPageApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + projectItemsCursor: 'cursor-1', + includeAuth: false, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ + fieldId: 'PVTSSF_lAHOABCD1234', + fieldName: 'Status', + optionId: 'f75ad846', + name: 'In Progress', + }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' })]), + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project get-status', + input: { item_type: 'issue', item_number: 13 }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(firstPageApi.isDone(), true); + assert.equal(secondPageApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore paginates project fields so later matching fields are found', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ + fieldId: 'PVTSSF_lAHOABCD1234', + fieldName: 'Status', + optionId: 'f75ad846', + name: 'In Progress', + }), + }), + ]), + }, + }, + }, + }, + }); + const firstFieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })], { + hasNextPage: true, + endCursor: 'fields-cursor-1', + }), + }, + }, + }, + }); + const secondFieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + fieldsCursor: 'fields-cursor-1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' })]), + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project get-status', + input: { item_type: 'issue', item_number: 13 }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(itemApi.isDone(), true); + assert.equal(firstFieldsApi.isDone(), true); + assert.equal(secondFieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore reads project status for a pull request and returns structured success output', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'pr', + itemNumber: 9, + graphqlResponseBody: { + data: { + repository: { + pullRequest: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_pr1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ + fieldId: 'PVTSSF_lAHOABCD1234', + fieldName: 'Status', + optionId: 'f75ad846', + name: 'In Progress', + }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' })]), + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project get-status', + input: { item_type: 'pr', item_number: 9 }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore fails clearly when the target issue is not on the configured project', async () => { + await withNock(async () => { + const api = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ id: 'PVTI_elsewhere', projectId: 'PVT_elsewhere', projectOwner: 'throw-if-null', projectNumber: 99 }), + ]), + }, + }, + }, + }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'project get-status', + input: { item_type: 'issue', item_number: 13 }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + /Issue #13 is not present on GitHub Project throw-if-null\/1\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore fails clearly when the configured project is missing the Status field', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + fields: [createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })], + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })]), + }, + }, + }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'project get-status', + input: { item_type: 'issue', item_number: 13 }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + /GitHub Project throw-if-null\/1 has no single-select field named "Status"\./, + ); + + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore maps project get-status auth failures clearly', async () => { + await withNock(async () => { + const api = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlStatus: 403, + graphqlResponseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'project get-status', + input: { item_type: 'issue', item_number: 13 }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + /GitHub App authentication failed while reading project status for issue #13\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore supports explicit status field overrides for project get-status', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + statusFieldName: 'Delivery', + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + fields: [createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })], + statusValue: createProjectStatusValueNode({ + fieldId: 'PVTSSF_delivery', + fieldName: 'Delivery', + optionId: 'f75ad847', + name: 'Shipped', + }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })]), + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project get-status', + input: { item_type: 'issue', item_number: 13, status_field_name: 'Delivery' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); diff --git a/src/commands/project/set-status/handler.test.ts b/src/commands/project/set-status/handler.test.ts new file mode 100644 index 0000000..d5928a4 --- /dev/null +++ b/src/commands/project/set-status/handler.test.ts @@ -0,0 +1,507 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; +import { withNock } from '../../../../test/support/http-test.js'; +import { + createProjectFieldsConnection, + createProjectItemNode, + createProjectItemsConnection, + createProjectStatusFieldNode, + createProjectStatusValueNode, + mockProjectGetStatusRequest, + mockProjectStatusFieldsRequest, + mockProjectStatusUpdateRequest, +} from '../../../../test/project/fixtures.js'; +import { createRepoConfigWithDefaultProject } from '../../../../test/support/runtime-fixtures.js'; + +test('runOrfeCore sets project status for an issue and returns structured success output', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_lAHOABCD1234', fieldName: 'Status', optionId: 'f75ad845', name: 'Todo' }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + const observedFieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + const mutationApi = mockProjectStatusUpdateRequest({ + projectId: 'PVT_project_1', + itemId: 'PVTI_lAHOABCD1234', + fieldId: 'PVTSSF_lAHOABCD1234', + optionId: 'f75ad846', + }); + const observedItemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + includeAuth: false, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_lAHOABCD1234', fieldName: 'Status', optionId: 'f75ad846', name: 'In Progress' }), + }), + ]), + }, + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project set-status', + input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.deepEqual(result, { + ok: true, + command: 'project set-status', + repo: 'throw-if-null/orfe', + data: { + project_owner: 'throw-if-null', + project_number: 1, + status_field_name: 'Status', + status_field_id: 'PVTSSF_lAHOABCD1234', + item_type: 'issue', + item_number: 13, + project_item_id: 'PVTI_lAHOABCD1234', + status_option_id: 'f75ad846', + status: 'In Progress', + previous_status_option_id: 'f75ad845', + previous_status: 'Todo', + changed: true, + }, + }); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + assert.equal(mutationApi.isDone(), true); + assert.equal(observedItemApi.isDone(), true); + assert.equal(observedFieldsApi.isDone(), true); + }); +}); + +test('executeOrfeTool returns the shared success envelope for project set-status', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_lAHOABCD1234', fieldName: 'Status', optionId: 'f75ad845', name: 'Todo' }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + const observedFieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + const mutationApi = mockProjectStatusUpdateRequest({ + projectId: 'PVT_project_1', + itemId: 'PVTI_lAHOABCD1234', + fieldId: 'PVTSSF_lAHOABCD1234', + optionId: 'f75ad846', + }); + const observedItemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + includeAuth: false, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_lAHOABCD1234', fieldName: 'Status', optionId: 'f75ad846', name: 'In Progress' }), + }), + ]), + }, + }, + }, + }, + }); + + const result = await runToolCommand({ + input: { command: 'project set-status', item_type: 'issue', item_number: 13, status: 'In Progress' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + assert.equal(mutationApi.isDone(), true); + assert.equal(observedItemApi.isDone(), true); + assert.equal(observedFieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore treats project set-status as an idempotent no-op when the requested status already matches', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_lAHOABCD1234', fieldName: 'Status', optionId: 'f75ad846', name: 'In Progress' }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project set-status', + input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore fails clearly when project set-status targets an item outside the configured project', async () => { + await withNock(async () => { + const api = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ id: 'PVTI_elsewhere', projectId: 'PVT_elsewhere', projectOwner: 'throw-if-null', projectNumber: 99 }), + ]), + }, + }, + }, + }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'project set-status', + input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + /Issue #13 is not present on GitHub Project throw-if-null\/1\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore fails clearly when project set-status receives an invalid status option', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_lAHOABCD1234', fieldName: 'Status', optionId: 'f75ad845', name: 'Todo' }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), + options: [ + { id: 'f75ad845', name: 'Todo' }, + { id: 'f75ad846', name: 'In Progress' }, + ], + }, + ]), + }, + }, + }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'project set-status', + input: { item_type: 'issue', item_number: 13, status: 'Done' }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + /GitHub Project throw-if-null\/1 field "Status" has no option named "Done"\./, + ); + + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + }); +}); + +test('runOrfeCore maps project set-status auth failures clearly', async () => { + await withNock(async () => { + const api = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + graphqlStatus: 403, + graphqlResponseBody: { message: 'Resource not accessible by integration' }, + }); + + await assert.rejects( + runCoreCommand({ + command: 'project set-status', + input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, + repoConfig: createRepoConfigWithDefaultProject(), + }), + /GitHub App authentication failed while setting project status for issue #13\./, + ); + + assert.equal(api.isDone(), true); + }); +}); + +test('runOrfeCore supports explicit status field overrides for project set-status', async () => { + await withNock(async () => { + const itemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + statusFieldName: 'Delivery', + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_delivery', fieldName: 'Delivery', optionId: 'f75ad847', name: 'Queued' }), + }), + ]), + }, + }, + }, + }, + }); + const fieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' }), + options: [ + { id: 'f75ad847', name: 'Queued' }, + { id: 'f75ad848', name: 'Shipped' }, + ], + }, + ]), + }, + }, + }, + }); + const observedFieldsApi = mockProjectStatusFieldsRequest({ + projectId: 'PVT_project_1', + graphqlResponseBody: { + data: { + node: { + fields: createProjectFieldsConnection([ + { + ...createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' }), + options: [ + { id: 'f75ad847', name: 'Queued' }, + { id: 'f75ad848', name: 'Shipped' }, + ], + }, + ]), + }, + }, + }, + }); + const mutationApi = mockProjectStatusUpdateRequest({ + projectId: 'PVT_project_1', + itemId: 'PVTI_lAHOABCD1234', + fieldId: 'PVTSSF_delivery', + optionId: 'f75ad848', + }); + const observedItemApi = mockProjectGetStatusRequest({ + itemType: 'issue', + itemNumber: 13, + statusFieldName: 'Delivery', + includeAuth: false, + graphqlResponseBody: { + data: { + repository: { + issue: { + projectItems: createProjectItemsConnection([ + createProjectItemNode({ + id: 'PVTI_lAHOABCD1234', + projectId: 'PVT_project_1', + projectOwner: 'throw-if-null', + projectNumber: 1, + statusValue: createProjectStatusValueNode({ fieldId: 'PVTSSF_delivery', fieldName: 'Delivery', optionId: 'f75ad848', name: 'Shipped' }), + }), + ]), + }, + }, + }, + }, + }); + + const result = await runCoreCommand({ + command: 'project set-status', + input: { item_type: 'issue', item_number: 13, status_field_name: 'Delivery', status: 'Shipped' }, + repoConfig: createRepoConfigWithDefaultProject(), + }); + + assert.equal(result.ok, true); + assert.equal(itemApi.isDone(), true); + assert.equal(fieldsApi.isDone(), true); + assert.equal(mutationApi.isDone(), true); + assert.equal(observedItemApi.isDone(), true); + assert.equal(observedFieldsApi.isDone(), true); + }); +}); diff --git a/test/auth/fixtures.ts b/test/auth/fixtures.ts new file mode 100644 index 0000000..1dd5785 --- /dev/null +++ b/test/auth/fixtures.ts @@ -0,0 +1,23 @@ +import nock from 'nock'; + +export function mockAuthTokenMintRequest(options: { + repo?: { owner: string; name: string }; + installationStatus?: number; + tokenStatus?: number; +} = {}) { + const owner = options.repo?.owner ?? 'throw-if-null'; + const repo = options.repo?.name ?? 'orfe'; + + const scope = nock('https://api.github.com').get(`/repos/${owner}/${repo}/installation`).reply(options.installationStatus ?? 200, { + id: 42, + }); + + if ((options.installationStatus ?? 200) === 200) { + scope.post('/app/installations/42/access_tokens').reply(options.tokenStatus ?? 201, { + token: 'ghs_123', + expires_at: '2026-04-06T12:00:00Z', + }); + } + + return scope; +} diff --git a/test/core.test.ts b/test/core.test.ts deleted file mode 100644 index 0fe12d8..0000000 --- a/test/core.test.ts +++ /dev/null @@ -1,5875 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import nock from 'nock'; -import { test } from 'vitest'; -import { fileURLToPath } from 'node:url'; - -import { COMMANDS } from '../src/commands/index.js'; -import { createHelpCommandSuccessData, createHelpRootSuccessData } from '../src/commands/help/definition.js'; -import { OrfeError } from '../src/errors.js'; -import { GitHubClientFactory } from '../src/github.js'; -import { runOrfeCore } from '../src/core.js'; - -const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const repoConfigPath = path.join(workspaceRoot, '.orfe', 'config.json'); - -function createRepoConfig() { - return { - configPath: repoConfigPath, - version: 1 as const, - repository: { - owner: 'throw-if-null', - name: 'orfe', - defaultBranch: 'main', - }, - callerToBot: { - Greg: 'greg', - }, - projects: { - default: { - owner: 'throw-if-null', - projectNumber: 1, - statusFieldName: 'Status', - }, - }, - }; -} - -function createAuthConfig() { - return { - configPath: '/tmp/auth.json', - version: 1 as const, - bots: { - greg: { - provider: 'github-app' as const, - appId: 123458, - appSlug: 'GR3G-BOT', - privateKeyPath: '/tmp/greg.pem', - }, - }, - }; -} - -function createGitHubClientFactory() { - return new GitHubClientFactory({ - readFileImpl: async () => 'private-key', - jwtFactory: () => 'jwt-token', - }); -} - -function mockAuthTokenMintRequest(options: { repo?: { owner: string; name: string }; installationStatus?: number; tokenStatus?: number }) { - const owner = options.repo?.owner ?? 'throw-if-null'; - const repo = options.repo?.name ?? 'orfe'; - - const scope = nock('https://api.github.com').get(`/repos/${owner}/${repo}/installation`).reply(options.installationStatus ?? 200, { - id: 42, - }); - - if ((options.installationStatus ?? 200) === 200) { - scope.post('/app/installations/42/access_tokens').reply(options.tokenStatus ?? 201, { - token: 'ghs_123', - expires_at: '2026-04-06T12:00:00Z', - }); - } - - return scope; -} - -function mockIssueGetRequest(options: { - issueNumber: number; - status?: number; - responseBody?: Record; -}) { - const issueNumber = options.issueNumber; - const status = options.status ?? 200; - - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) - .reply( - status, - options.responseBody ?? { - number: issueNumber, - title: 'Build `orfe` foundation and runtime scaffolding', - body: 'Issue body', - state: 'open', - state_reason: null, - labels: [{ name: 'needs-input' }], - assignees: [{ login: 'greg' }], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - }, - ); -} - -function mockIssueCommentRequest(options: { - issueNumber: number; - body: string; - status?: number; - responseBody?: Record; - issueGetStatus?: number; - issueGetResponseBody?: Record; -}) { - const issueNumber = options.issueNumber; - const status = options.status ?? 201; - const issueGetStatus = options.issueGetStatus ?? 200; - - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) - .reply( - issueGetStatus, - options.issueGetResponseBody ?? { - number: issueNumber, - title: 'Issue title', - body: 'Issue body', - state: 'open', - state_reason: null, - labels: [], - assignees: [], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - }, - ) - .post(`/repos/throw-if-null/orfe/issues/${issueNumber}/comments`, { body: options.body }) - .reply( - status, - options.responseBody ?? { - id: 123456, - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}#issuecomment-123456`, - }, - ); -} - -function mockIssueCreateRequest(options: { - requestBody: Record; - status?: number; - responseBody?: Record; - repo?: { owner: string; name: string }; -}) { - const owner = options.repo?.owner ?? 'throw-if-null'; - const repo = options.repo?.name ?? 'orfe'; - const status = options.status ?? 201; - - return nock('https://api.github.com') - .get(`/repos/${owner}/${repo}/installation`) - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .post(`/repos/${owner}/${repo}/issues`, options.requestBody) - .reply( - status, - options.responseBody ?? { - number: 21, - node_id: 'I_kwDOOrfeIssue21', - title: options.requestBody.title, - body: options.requestBody.body ?? '', - state: 'open', - state_reason: null, - labels: ((options.requestBody.labels as string[] | undefined) ?? []).map((name) => ({ name })), - assignees: ((options.requestBody.assignees as string[] | undefined) ?? []).map((login) => ({ login })), - html_url: `https://github.com/${owner}/${repo}/issues/21`, - }, - ); -} - -function matchesProjectByOwnerAndNumber(body: unknown, options: { projectOwner: string; projectNumber: number }): boolean { - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes('query ProjectByOwnerAndNumber') && - isObject(body.variables) && - body.variables.login === options.projectOwner && - body.variables.number === options.projectNumber - ); -} - -function createProjectLookupResponse(options: { projectId: string; ownerType?: 'organization' | 'user' }) { - return { - data: { - repositoryOwner: { - __typename: options.ownerType === 'user' ? 'User' : 'Organization', - projectV2: { - id: options.projectId, - }, - }, - }, - }; -} - -function matchesProjectAddItem(body: unknown, options: { projectId: string; contentId: string }): boolean { - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes('mutation AddProjectItem') && - isObject(body.variables) && - body.variables.projectId === options.projectId && - body.variables.contentId === options.contentId - ); -} - -function renderIssueBodyContractMarker(): string { - return ''; -} - -function renderPrBodyContractMarker(): string { - return ''; -} - -function mockIssueUpdateRequest(options: { - issueNumber: number; - requestBody: Record; - status?: number; - responseBody?: Record; - issueGetStatus?: number; - issueGetResponseBody?: Record; -}) { - const issueNumber = options.issueNumber; - const status = options.status ?? 200; - const issueGetStatus = options.issueGetStatus ?? 200; - - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) - .reply( - issueGetStatus, - options.issueGetResponseBody ?? { - number: issueNumber, - title: 'Updated title', - body: 'Updated body', - state: 'open', - state_reason: null, - labels: [{ name: 'bug' }, { name: 'needs-input' }], - assignees: [{ login: 'greg' }], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - }, - ) - .patch(`/repos/throw-if-null/orfe/issues/${issueNumber}`, options.requestBody) - .reply( - status, - options.responseBody ?? { - number: issueNumber, - title: 'Updated title', - body: 'Updated body', - state: 'open', - state_reason: null, - labels: [{ name: 'bug' }, { name: 'needs-input' }], - assignees: [{ login: 'greg' }], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - }, - ); -} - -function mockPullRequestGetRequest(options: { - prNumber: number; - status?: number; - responseBody?: Record; -}) { - const prNumber = options.prNumber; - const status = options.status ?? 200; - - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply( - status, - options.responseBody ?? { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }, - ); -} - -function mockPullRequestGetOrCreateRequest(options: { - head: string; - base?: string; - existingPullRequests?: Record[]; - listStatus?: number; - listResponseBody?: unknown; - createRequestBody?: Record; - createStatus?: number; - createResponseBody?: Record; -}) { - const head = options.head; - const base = options.base ?? 'main'; - const scope = nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get('/repos/throw-if-null/orfe/pulls') - .query({ state: 'open', head: `throw-if-null:${head}`, base, per_page: 100 }) - .reply(options.listStatus ?? 200, options.listResponseBody ?? options.existingPullRequests ?? []); - - if (options.createStatus !== undefined || options.createResponseBody !== undefined || options.createRequestBody !== undefined) { - scope - .post('/repos/throw-if-null/orfe/pulls', options.createRequestBody ?? { - head, - base, - title: 'Design the `orfe` custom tool and CLI contract', - draft: false, - }) - .reply( - options.createStatus ?? 201, - options.createResponseBody ?? { - number: 9, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: head }, - base: { ref: base }, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - ); - } - - return scope; -} - -function mockPullRequestCommentRequest(options: { - prNumber: number; - body: string; - verifyStatus?: number; - verifyResponseBody?: Record; - status?: number; - responseBody?: Record; -}) { - const prNumber = options.prNumber; - const verifyStatus = options.verifyStatus ?? 200; - const status = options.status ?? 201; - - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply( - verifyStatus, - options.verifyResponseBody ?? { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }, - ) - .post(`/repos/throw-if-null/orfe/issues/${prNumber}/comments`, { body: options.body }) - .reply( - status, - options.responseBody ?? { - id: 123456, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}#issuecomment-123456`, - }, - ); -} - -function mockPullRequestReplyRequest(options: { - prNumber: number; - commentId: number; - body: string; - verifyStatus?: number; - verifyResponseBody?: Record; - status?: number; - responseBody?: Record; -}) { - const prNumber = options.prNumber; - const commentId = options.commentId; - const verifyStatus = options.verifyStatus ?? 200; - const status = options.status ?? 201; - - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply( - verifyStatus, - options.verifyResponseBody ?? { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }, - ) - .post(`/repos/throw-if-null/orfe/pulls/${prNumber}/comments/${commentId}/replies`, { body: options.body }) - .reply( - status, - options.responseBody ?? { - id: 123999, - in_reply_to_id: commentId, - }, - ); -} - -function mockPullRequestSubmitReviewRequest(options: { - prNumber: number; - body: string; - event: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT'; - verifyStatus?: number; - verifyResponseBody?: Record; - status?: number; - responseBody?: Record; -}) { - const prNumber = options.prNumber; - const verifyStatus = options.verifyStatus ?? 200; - const status = options.status ?? 200; - - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply( - verifyStatus, - options.verifyResponseBody ?? { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }, - ) - .post(`/repos/throw-if-null/orfe/pulls/${prNumber}/reviews`, { - body: options.body, - event: options.event, - }) - .reply( - status, - options.responseBody ?? { - id: 555, - }, - ); -} - -function createIssueRestResponse(issueNumber: number, overrides: Record = {}) { - return { - number: issueNumber, - title: 'Issue title', - body: 'Issue body', - state: 'open', - state_reason: null, - labels: [], - assignees: [], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - ...overrides, - }; -} - -function createIssueStateNode(options: { - id: string; - issueNumber: number; - state: string; - stateReason?: string | null; - duplicateOfIssueNumber?: number; - duplicateOfId?: string; -}) { - return { - id: options.id, - number: options.issueNumber, - state: options.state, - stateReason: options.stateReason ?? null, - duplicateOf: - options.duplicateOfIssueNumber !== undefined - ? { - id: options.duplicateOfId ?? `I_${options.duplicateOfIssueNumber}`, - number: options.duplicateOfIssueNumber, - } - : null, - }; -} - -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -function matchesIssueStateLookup(body: unknown, issueNumber: number): boolean { - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes('query IssueStateByNumber') && - isObject(body.variables) && - body.variables.issueNumber === issueNumber - ); -} - -function matchesMarkIssueAsDuplicate(body: unknown, duplicateId: string, canonicalId: string): boolean { - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes('mutation MarkIssueAsDuplicate') && - isObject(body.variables) && - body.variables.duplicateId === duplicateId && - body.variables.canonicalId === canonicalId - ); -} - -function matchesProjectStatusLookup(body: unknown, options: { itemType: 'issue' | 'pr'; itemNumber: number; statusFieldName: string }): boolean { - const expectedQueryName = options.itemType === 'issue' ? 'query ProjectStatusForIssue' : 'query ProjectStatusForPullRequest'; - - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes(expectedQueryName) && - isObject(body.variables) && - body.variables.itemNumber === options.itemNumber && - body.variables.statusFieldName === options.statusFieldName - ); -} - -function matchesProjectStatusFields(body: unknown, options: { projectId: string; fieldsCursor?: string | null }): boolean { - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes('query ProjectStatusFields') && - isObject(body.variables) && - body.variables.projectId === options.projectId && - body.variables.fieldsCursor === (options.fieldsCursor ?? null) - ); -} - -function matchesProjectStatusUpdate(body: unknown, options: { projectId: string; itemId: string; fieldId: string; optionId: string }): boolean { - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes('mutation UpdateProjectStatus') && - isObject(body.variables) && - body.variables.projectId === options.projectId && - body.variables.itemId === options.itemId && - body.variables.fieldId === options.fieldId && - body.variables.optionId === options.optionId - ); -} - -function createPageInfo(options?: { hasNextPage?: boolean; endCursor?: string | null }) { - return { - hasNextPage: options?.hasNextPage ?? false, - endCursor: options?.endCursor ?? null, - }; -} - -function createProjectItemsConnection(nodes: unknown[], options?: { hasNextPage?: boolean; endCursor?: string | null }) { - return { - nodes, - pageInfo: createPageInfo(options), - }; -} - -function createProjectFieldsConnection(nodes: unknown[], options?: { hasNextPage?: boolean; endCursor?: string | null }) { - return { - nodes, - pageInfo: createPageInfo(options), - }; -} - -function createProjectStatusFieldNode(options: { id: string; name: string }) { - return { - __typename: 'ProjectV2SingleSelectField', - id: options.id, - name: options.name, - }; -} - -function createProjectStatusValueNode(options: { fieldId: string; fieldName: string; optionId: string; name: string }) { - return { - __typename: 'ProjectV2ItemFieldSingleSelectValue', - optionId: options.optionId, - name: options.name, - field: { - __typename: 'ProjectV2SingleSelectField', - id: options.fieldId, - name: options.fieldName, - }, - }; -} - -function createProjectItemNode(options: { - id: string; - projectId?: string; - projectOwner: string; - projectNumber: number; - fields?: unknown[]; - statusValue?: unknown; -}) { - return { - id: options.id, - project: { - id: options.projectId ?? 'PVT_project_1', - number: options.projectNumber, - owner: { - login: options.projectOwner, - }, - fields: { - nodes: options.fields ?? [], - }, - }, - fieldValueByName: options.statusValue ?? null, - }; -} - -function mockProjectGetStatusRequest(options: { - itemType: 'issue' | 'pr'; - itemNumber: number; - statusFieldName?: string; - graphqlStatus?: number; - graphqlResponseBody?: Record; - projectItemsCursor?: string | null; - includeAuth?: boolean; -}) { - const statusFieldName = options.statusFieldName ?? 'Status'; - - let scope = nock('https://api.github.com'); - if (options.includeAuth !== false) { - scope = scope - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }); - } - - return scope - .post('/graphql', (body: unknown) => - matchesProjectStatusLookup(body, { - itemType: options.itemType, - itemNumber: options.itemNumber, - statusFieldName, - }) && isObject(body) && isObject(body.variables) && body.variables.projectItemsCursor === (options.projectItemsCursor ?? null), - ) - .reply( - options.graphqlStatus ?? 200, - options.graphqlResponseBody ?? { - data: { - repository: - options.itemType === 'issue' - ? { - issue: { - projectItems: createProjectItemsConnection([]), - }, - } - : { - pullRequest: { - projectItems: createProjectItemsConnection([]), - }, - }, - }, - }, - ); -} - -function mockProjectStatusFieldsRequest(options: { - projectId?: string; - fieldsCursor?: string | null; - graphqlStatus?: number; - graphqlResponseBody?: Record; -}) { - return nock('https://api.github.com') - .post('/graphql', (body: unknown) => - matchesProjectStatusFields(body, { - projectId: options.projectId ?? 'PVT_project_1', - ...(options.fieldsCursor !== undefined ? { fieldsCursor: options.fieldsCursor } : {}), - }), - ) - .reply( - options.graphqlStatus ?? 200, - options.graphqlResponseBody ?? { - data: { - node: { - fields: createProjectFieldsConnection([]), - }, - }, - }, - ); -} - -function mockProjectStatusUpdateRequest(options: { - projectId?: string; - itemId: string; - fieldId: string; - optionId: string; - graphqlStatus?: number; - graphqlResponseBody?: Record; -}) { - return nock('https://api.github.com') - .post('/graphql', (body: unknown) => - matchesProjectStatusUpdate(body, { - projectId: options.projectId ?? 'PVT_project_1', - itemId: options.itemId, - fieldId: options.fieldId, - optionId: options.optionId, - }), - ) - .reply( - options.graphqlStatus ?? 200, - options.graphqlResponseBody ?? { - data: { - updateProjectV2ItemFieldValue: { - clientMutationId: null, - }, - }, - }, - ); -} - -function mockProjectLookupRequest(options: { - projectOwner: string; - projectNumber: number; - projectId?: string; - graphqlStatus?: number; - graphqlResponseBody?: Record; - includeAuth?: boolean; -}) { - let scope = nock('https://api.github.com'); - if (options.includeAuth !== false) { - scope = scope - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }); - } - - return scope - .post('/graphql', (body: unknown) => - matchesProjectByOwnerAndNumber(body, { - projectOwner: options.projectOwner, - projectNumber: options.projectNumber, - }), - ) - .reply( - options.graphqlStatus ?? 200, - options.graphqlResponseBody ?? createProjectLookupResponse({ projectId: options.projectId ?? 'PVT_project_1' }), - ); -} - -function mockProjectAddItemRequest(options: { - projectId?: string; - contentId: string; - projectItemId?: string; - graphqlStatus?: number; - graphqlResponseBody?: Record; - includeAuth?: boolean; -}) { - let scope = nock('https://api.github.com'); - if (options.includeAuth !== false) { - scope = scope - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }); - } - - return scope - .post('/graphql', (body: unknown) => - matchesProjectAddItem(body, { - projectId: options.projectId ?? 'PVT_project_1', - contentId: options.contentId, - }), - ) - .reply( - options.graphqlStatus ?? 200, - options.graphqlResponseBody ?? { - data: { - addProjectV2ItemById: { - item: { - id: options.projectItemId ?? 'PVTI_lAHOABCD1234', - }, - }, - }, - }, - ); -} - -function matchesUnmarkIssueAsDuplicate(body: unknown, duplicateId: string, canonicalId: string): boolean { - return ( - isObject(body) && - typeof body.query === 'string' && - body.query.includes('mutation UnmarkIssueAsDuplicate') && - isObject(body.variables) && - body.variables.duplicateId === duplicateId && - body.variables.canonicalId === canonicalId - ); -} - -function mockIssueSetStateRequest(options: { - issueNumber: number; - currentIssueState: Record; - restUpdateBody?: Record; - observedIssueState?: Record; - issueGetStatus?: number; - issueGetResponseBody?: Record; - includeGraphql?: boolean; - unmark?: { duplicateId: string; canonicalId: string }; -}) { - const scope = nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`) - .reply(options.issueGetStatus ?? 200, options.issueGetResponseBody ?? createIssueRestResponse(options.issueNumber)); - - if (options.includeGraphql !== false) { - scope.post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)).reply(200, { - data: { repository: { issue: options.currentIssueState } }, - }); - } - - if (options.unmark) { - scope - .post('/graphql', (body: unknown) => matchesUnmarkIssueAsDuplicate(body, options.unmark!.duplicateId, options.unmark!.canonicalId)) - .reply(200, { data: { unmarkIssueAsDuplicate: { clientMutationId: null } } }); - } - - if (options.restUpdateBody) { - scope - .patch(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`, options.restUpdateBody) - .reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)) - .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)) - .reply(200, { data: { repository: { issue: options.observedIssueState ?? options.currentIssueState } } }); - } - - return scope; -} - -function mockIssueSetStateDuplicateRequest(options: { - issueNumber: number; - duplicateOfIssueNumber: number; - currentIssueState: Record; - canonicalIssueState: Record | null; - duplicateTargetGetStatus?: number; - duplicateTargetGetResponseBody?: Record; - unmark?: { duplicateId: string; canonicalId: string }; - mark?: { duplicateId: string; canonicalId: string }; - restUpdateBody?: Record; - observedIssueState?: Record; - issueGetStatus?: number; - issueGetResponseBody?: Record; - includeGraphql?: boolean; -}) { - const scope = nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`) - .reply(options.issueGetStatus ?? 200, options.issueGetResponseBody ?? createIssueRestResponse(options.issueNumber)); - - if (options.includeGraphql !== false) { - scope - .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)) - .reply(200, { data: { repository: { issue: options.currentIssueState } } }) - .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.duplicateOfIssueNumber)) - .reply(200, { data: { repository: { issue: options.canonicalIssueState } } }); - } - - if (options.canonicalIssueState === null) { - scope - .get(`/repos/throw-if-null/orfe/issues/${options.duplicateOfIssueNumber}`) - .reply(options.duplicateTargetGetStatus ?? 404, options.duplicateTargetGetResponseBody ?? { message: 'Not Found' }); - } - - if (options.unmark) { - scope - .post('/graphql', (body: unknown) => matchesUnmarkIssueAsDuplicate(body, options.unmark!.duplicateId, options.unmark!.canonicalId)) - .reply(200, { data: { unmarkIssueAsDuplicate: { clientMutationId: null } } }); - } - - if (options.mark) { - scope - .post('/graphql', (body: unknown) => matchesMarkIssueAsDuplicate(body, options.mark!.duplicateId, options.mark!.canonicalId)) - .reply(200, { data: { markIssueAsDuplicate: { clientMutationId: null } } }); - } - - if (options.observedIssueState) { - if (options.restUpdateBody) { - scope.patch(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`, options.restUpdateBody).reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)); - } - - scope - .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)) - .reply(200, { data: { repository: { issue: options.observedIssueState } } }); - } - - return scope; -} - -test('runOrfeCore mints an auth token for the resolved caller bot', async () => { - nock.disableNetConnect(); - - try { - const api = mockAuthTokenMintRequest({ repo: { owner: 'throw-if-null', name: 'orfe' } }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'auth token', - input: { - repo: 'throw-if-null/orfe', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'auth token', - repo: 'throw-if-null/orfe', - data: { - bot: 'greg', - app_slug: 'GR3G-BOT', - repo: 'throw-if-null/orfe', - token: 'ghs_123', - expires_at: '2026-04-06T12:00:00Z', - auth_mode: 'github-app', - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore returns runtime info without caller, config, auth, or GitHub access', async () => { - const result = await runOrfeCore( - { - callerName: '', - command: 'runtime info', - input: {}, - entrypoint: 'cli', - }, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - assert.equal(result.command, 'runtime info'); - assert.equal(result.repo, undefined); - const data = result.data as { orfe_version: string; entrypoint: string }; - assert.match(data.orfe_version, /^\d+\.\d+\.\d+/); - assert.deepEqual(data, { - orfe_version: data.orfe_version, - entrypoint: 'cli', - }); -}); - -test('runOrfeCore returns root help without caller, config, auth, or GitHub access', async () => { - const result = await runOrfeCore( - { - callerName: '', - command: 'help', - input: {}, - entrypoint: 'opencode-plugin', - }, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - assert.equal(result.command, 'help'); - assert.equal(result.repo, undefined); - assert.deepEqual(result.data, createHelpRootSuccessData(COMMANDS)); -}); - -test('runOrfeCore returns targeted command help without caller, config, auth, or GitHub access', async () => { - const result = await runOrfeCore( - { - callerName: '', - command: 'help', - input: { command_name: 'runtime info' }, - entrypoint: 'opencode-plugin', - }, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - assert.equal(result.command, 'help'); - assert.equal(result.repo, undefined); - assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, 'runtime info')); -}); - -test('runOrfeCore returns targeted command help with explicit requirements for representative commands', async () => { - for (const commandName of ['issue get', 'pr get-or-create', 'project set-status'] as const) { - const result = await runOrfeCore( - { - callerName: '', - command: 'help', - input: { command_name: commandName }, - entrypoint: 'opencode-plugin', - }, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - assert.equal(result.command, 'help'); - assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, commandName)); - } -}); - -test('runOrfeCore can validate issue bodies without auth config or GitHub access', async () => { - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue validate', - input: { - body: [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: update existing docs', - '- Details: update docs/orfe/spec.md', - '', - '## ADR needed?', - '', - '- ADR needed: no', - '- Details: covered by ADR 0009', - '', - '## Dependencies / sequencing notes', - '', - '- depends on #59', - '', - '## Risks / open questions / non-goals', - '', - '- keep repo-specific structure out of runtime logic', - ].join('\n'), - body_contract: 'formal-work-item@1.0.0', - }, - entrypoint: 'opencode-plugin', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - assert.equal(result.command, 'issue validate'); - assert.equal(result.repo, 'throw-if-null/orfe'); - if (result.ok) { - assert.equal((result.data as { valid: boolean }).valid, true); - } -}); - -test('runOrfeCore rejects bot override input for auth token', async () => { - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'auth token', - input: { bot: 'unknown', repo: 'throw-if-null/orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'invalid_usage'); - assert.equal(error.message, 'Command "auth token" does not accept input field "bot".'); - return true; - }, - ); -}); - -test('runOrfeCore fails clearly for auth token when the caller is unmapped', async () => { - await assert.rejects( - runOrfeCore( - { - callerName: 'Unknown Agent', - command: 'auth token', - input: { repo: 'throw-if-null/orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'caller_name_unmapped'); - assert.match(error.message, /Caller name "Unknown Agent" is not mapped/); - return true; - }, - ); -}); - -test('runOrfeCore fails clearly for auth token when the installation is missing', async () => { - nock.disableNetConnect(); - - try { - const api = mockAuthTokenMintRequest({ installationStatus: 404 }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'auth token', - input: { repo: 'throw-if-null/orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'No GitHub App installation for throw-if-null/orfe was found for app GR3G-BOT.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore fails clearly for auth token when token minting is rejected', async () => { - nock.disableNetConnect(); - - try { - const api = mockAuthTokenMintRequest({ tokenStatus: 403 }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'auth token', - input: { repo: 'throw-if-null/orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'Failed to mint an installation token for bot "greg" on throw-if-null/orfe.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore surfaces config failures for auth token clearly', async () => { - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'auth token', - input: { repo: 'throw-if-null/orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => { - throw new OrfeError('config_not_found', 'machine-local auth config not found at /tmp/auth.json.'); - }, - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'config_not_found'); - assert.equal(error.message, 'machine-local auth config not found at /tmp/auth.json.'); - return true; - }, - ); -}); - -test('runOrfeCore reads project status for an issue and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - fields: [createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' })], - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad846', - name: 'In Progress', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'issue', item_number: 13 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project get-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad846', - status: 'In Progress', - }, - }); - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore paginates project items so later matching items are found', async () => { - nock.disableNetConnect(); - - try { - const firstPageApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection( - [ - createProjectItemNode({ - id: 'PVTI_elsewhere', - projectId: 'PVT_elsewhere', - projectOwner: 'throw-if-null', - projectNumber: 99, - }), - ], - { hasNextPage: true, endCursor: 'cursor-1' }, - ), - }, - }, - }, - }, - }); - const secondPageApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - projectItemsCursor: 'cursor-1', - includeAuth: false, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad846', - name: 'In Progress', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'issue', item_number: 13 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - if (result.ok) { - assert.deepEqual(result.data, { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad846', - status: 'In Progress', - }); - } - assert.equal(firstPageApi.isDone(), true); - assert.equal(secondPageApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore paginates project fields so later matching fields are found', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad846', - name: 'In Progress', - }), - }), - ]), - }, - }, - }, - }, - }); - const firstFieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection( - [createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })], - { hasNextPage: true, endCursor: 'fields-cursor-1' }, - ), - }, - }, - }, - }); - const secondFieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - fieldsCursor: 'fields-cursor-1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'issue', item_number: 13 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - if (result.ok) { - assert.deepEqual(result.data, { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad846', - status: 'In Progress', - }); - } - assert.equal(itemApi.isDone(), true); - assert.equal(firstFieldsApi.isDone(), true); - assert.equal(secondFieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore reads project status for a pull request and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'pr', - itemNumber: 9, - graphqlResponseBody: { - data: { - repository: { - pullRequest: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_pr1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad846', - name: 'In Progress', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'pr', item_number: 9 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project get-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'pr', - item_number: 9, - project_item_id: 'PVTI_pr1234', - status_option_id: 'f75ad846', - status: 'In Progress', - }, - }); - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore fails clearly when the target issue is not on the configured project', async () => { - nock.disableNetConnect(); - - try { - const api = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_elsewhere', - projectId: 'PVT_elsewhere', - projectOwner: 'throw-if-null', - projectNumber: 99, - }), - ]), - }, - }, - }, - }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'issue', item_number: 13 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'project_item_not_found'); - assert.equal(error.message, 'Issue #13 is not present on GitHub Project throw-if-null/1.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore fails clearly when the configured project is missing the Status field', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - fields: [createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })], - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' }), - ]), - }, - }, - }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'issue', item_number: 13 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'project_status_field_not_found'); - assert.equal(error.message, 'GitHub Project throw-if-null/1 has no single-select field named "Status".'); - return true; - }, - ); - - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps project get-status auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlStatus: 403, - graphqlResponseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'issue', item_number: 13 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while reading project status for issue #13.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore supports explicit status field overrides for project get-status', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - statusFieldName: 'Delivery', - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - fields: [createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' })], - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_delivery', - fieldName: 'Delivery', - optionId: 'f75ad847', - name: 'Shipped', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' }), - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project get-status', - input: { item_type: 'issue', item_number: 13, status_field_name: 'Delivery' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project get-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Delivery', - status_field_id: 'PVTSSF_delivery', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad847', - status: 'Shipped', - }, - }); - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore validates issue-create bodies against explicit contracts and appends provenance', async () => { - nock.disableNetConnect(); - - try { - const issueBody = [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Agent-authored issues validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: add new durable docs', - '', - '## ADR needed?', - '', - '- ADR needed: yes', - ].join('\n'); - - const api = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - body: `${issueBody}\n\n${renderIssueBodyContractMarker()}`, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - body: issueBody, - body_contract: 'formal-work-item@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore allows provenance-only issue-update validation when no explicit contract is provided', async () => { - nock.disableNetConnect(); - - try { - const issueBody = [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Agent-authored issues validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: add new durable docs', - '', - '## ADR needed?', - '', - '- ADR needed: yes', - '', - renderIssueBodyContractMarker(), - ].join('\n'); - - const api = mockIssueUpdateRequest({ - issueNumber: 14, - requestBody: { - body: issueBody, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue update', - input: { - issue_number: 14, - body: issueBody, - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore returns structured issue validation results without GitHub access', async () => { - nock.disableNetConnect(); - - try { - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue validate', - input: { - body: [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: update existing docs', - '- Details: update docs/orfe/spec.md', - '', - '## ADR needed?', - '', - '- ADR needed: no', - '- Details: covered by ADR 0009', - '', - '## Dependencies / sequencing notes', - '', - '- depends on #59', - '', - '## Risks / open questions / non-goals', - '', - '- keep repo-specific structure out of runtime logic', - ].join('\n'), - body_contract: 'formal-work-item@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue validate', - repo: 'throw-if-null/orfe', - data: { - valid: true, - contract: { - artifact_type: 'issue', - contract_name: 'formal-work-item', - contract_version: '1.0.0', - }, - contract_source: 'explicit', - normalized_body: [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: update existing docs', - '- Details: update docs/orfe/spec.md', - '', - '## ADR needed?', - '', - '- ADR needed: no', - '- Details: covered by ADR 0009', - '', - '## Dependencies / sequencing notes', - '', - '- depends on #59', - '', - '## Risks / open questions / non-goals', - '', - '- keep repo-specific structure out of runtime logic', - '', - renderIssueBodyContractMarker(), - ].join('\n'), - errors: [], - }, - }); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore returns actionable issue validation failures', async () => { - nock.disableNetConnect(); - - try { - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue validate', - input: { - body: [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '## Docs impact', - '', - '- Docs impact: maybe', - '', - '## ADR needed?', - '', - '- ADR needed: no', - ].join('\n'), - body_contract: 'formal-work-item@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - assert.equal(result.command, 'issue validate'); - assert.equal(result.repo, 'throw-if-null/orfe'); - assert.equal((result.data as { valid: boolean }).valid, false); - assert.deepEqual( - (result.data as { errors: Array<{ kind: string }> }).errors.map((issue) => issue.kind), - ['missing_required_pattern', 'missing_required_section', 'invalid_allowed_value'], - ); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore validates PR bodies against explicit contracts and appends provenance on create', async () => { - nock.disableNetConnect(); - - try { - const prBody = [ - 'Ref: #59', - '', - '## Summary', - '', - '- add versioned body-contract support', - '', - '## Verification', - '', - '- `npm test` ✅', - '- `npm run lint` ✅', - '- `npm run typecheck` ✅', - '- `npm run build` ✅', - '', - '## Docs / ADR / debt', - '', - '- docs updated: yes', - '- ADR updated: yes', - '- debt updated: yes', - '- details: updated docs and added ADR', - '', - '## Risks / follow-ups', - '', - '- generation is still minimal in this slice', - ].join('\n'); - - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-59', - existingPullRequests: [], - createRequestBody: { - head: 'issues/orfe-59', - base: 'main', - title: 'Introduce versioned body-contract support', - body: `${prBody}\n\n${renderPrBodyContractMarker()}`, - draft: false, - }, - createResponseBody: { - number: 59, - title: 'Introduce versioned body-contract support', - body: `${prBody}\n\n${renderPrBodyContractMarker()}`, - state: 'open', - draft: false, - head: { ref: 'issues/orfe-59' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/59', - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { - head: 'issues/orfe-59', - title: 'Introduce versioned body-contract support', - body: prBody, - body_contract: 'implementation-ready@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore returns structured PR validation results without GitHub access', async () => { - nock.disableNetConnect(); - - try { - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr validate', - input: { - body: [ - 'Ref: #58', - '', - '## Summary', - '', - '- add PR body validation helpers', - '', - '## Verification', - '', - '- `npm test` ✅', - '- `npm run lint` ✅', - '- `npm run typecheck` ✅', - '- `npm run build` ✅', - '', - '## Docs / ADR / debt', - '', - '- docs updated: yes', - '- ADR updated: no', - '- debt updated: no', - '- details: updated docs/orfe/spec.md', - '', - '## Risks / follow-ups', - '', - '- none', - ].join('\n'), - body_contract: 'implementation-ready@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr validate', - repo: 'throw-if-null/orfe', - data: { - valid: true, - contract: { - artifact_type: 'pr', - contract_name: 'implementation-ready', - contract_version: '1.0.0', - }, - contract_source: 'explicit', - normalized_body: [ - 'Ref: #58', - '', - '## Summary', - '', - '- add PR body validation helpers', - '', - '## Verification', - '', - '- `npm test` ✅', - '- `npm run lint` ✅', - '- `npm run typecheck` ✅', - '- `npm run build` ✅', - '', - '## Docs / ADR / debt', - '', - '- docs updated: yes', - '- ADR updated: no', - '- debt updated: no', - '- details: updated docs/orfe/spec.md', - '', - '## Risks / follow-ups', - '', - '- none', - '', - renderPrBodyContractMarker(), - ].join('\n'), - errors: [], - }, - }); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore returns actionable PR validation failures', async () => { - nock.disableNetConnect(); - - try { - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr validate', - input: { - body: 'Ref: #58\n\nCloses: #58', - body_contract: 'implementation-ready@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - assert.equal(result.command, 'pr validate'); - assert.equal(result.repo, 'throw-if-null/orfe'); - assert.equal((result.data as { valid: boolean }).valid, false); - assert.deepEqual( - (result.data as { errors: Array<{ kind: string }> }).errors.map((issue) => issue.kind), - ['matched_forbidden_pattern', 'missing_required_section', 'missing_required_section', 'missing_required_section', 'missing_required_section'], - ); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore fails clearly when contract validation fails', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-59', - existingPullRequests: [], - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { - head: 'issues/orfe-59', - title: 'Introduce versioned body-contract support', - body: 'Ref: #59\n\nCloses: #59', - body_contract: 'implementation-ready@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'contract_validation_failed'); - assert.equal( - error.message, - 'Body contract validation failed: body matched forbidden pattern (?:^|\\n)(?:Closes|Close|Closed|Fixes|Fix|Fixed|Resolves|Resolve|Resolved)\\s*:?\\s*#\\d+.', - ); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore sets project status for an issue and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad845', - name: 'Todo', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - { id: 'f75ad847', name: 'Done' }, - ], - }, - ]), - }, - }, - }, - }); - const mutationApi = mockProjectStatusUpdateRequest({ - itemId: 'PVTI_lAHOABCD1234', - fieldId: 'PVTSSF_lAHOABCD1234', - optionId: 'f75ad846', - }); - const observedItemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - includeAuth: false, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad846', - name: 'In Progress', - }), - }), - ]), - }, - }, - }, - }, - }); - const observedFieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - { id: 'f75ad847', name: 'Done' }, - ], - }, - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project set-status', - input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project set-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad846', - status: 'In Progress', - previous_status_option_id: 'f75ad845', - previous_status: 'Todo', - changed: true, - }, - }); - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - assert.equal(mutationApi.isDone(), true); - assert.equal(observedItemApi.isDone(), true); - assert.equal(observedFieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore treats project set-status as an idempotent no-op when the requested status already matches', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad846', - name: 'In Progress', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - ], - }, - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project set-status', - input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project set-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad846', - status: 'In Progress', - previous_status_option_id: 'f75ad846', - previous_status: 'In Progress', - changed: false, - }, - }); - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore fails clearly when project set-status targets an item outside the configured project', async () => { - nock.disableNetConnect(); - - try { - const api = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([]), - }, - }, - }, - }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'project set-status', - input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'project_item_not_found'); - assert.equal(error.message, 'Issue #13 is not present on GitHub Project throw-if-null/1.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore fails clearly when project set-status receives an invalid status option', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad845', - name: 'Todo', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - ], - }, - ]), - }, - }, - }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'project set-status', - input: { item_type: 'issue', item_number: 13, status: 'Blocked' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'project_status_option_not_found'); - assert.equal(error.message, 'GitHub Project throw-if-null/1 field "Status" has no option named "Blocked".'); - return true; - }, - ); - - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps project set-status auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - graphqlStatus: 403, - graphqlResponseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'project set-status', - input: { item_type: 'issue', item_number: 13, status: 'In Progress' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while setting project status for issue #13.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore supports explicit status field overrides for project set-status', async () => { - nock.disableNetConnect(); - - try { - const itemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - statusFieldName: 'Delivery', - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_delivery', - fieldName: 'Delivery', - optionId: 'f75ad847', - name: 'Queued', - }), - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' }), - options: [ - { id: 'f75ad847', name: 'Queued' }, - { id: 'f75ad848', name: 'Shipped' }, - ], - }, - ]), - }, - }, - }, - }); - const mutationApi = mockProjectStatusUpdateRequest({ - itemId: 'PVTI_lAHOABCD1234', - fieldId: 'PVTSSF_delivery', - optionId: 'f75ad848', - }); - const observedItemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 13, - statusFieldName: 'Delivery', - includeAuth: false, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_delivery', - fieldName: 'Delivery', - optionId: 'f75ad848', - name: 'Shipped', - }), - }), - ]), - }, - }, - }, - }, - }); - const observedFieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_delivery', name: 'Delivery' }), - options: [ - { id: 'f75ad847', name: 'Queued' }, - { id: 'f75ad848', name: 'Shipped' }, - ], - }, - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'project set-status', - input: { item_type: 'issue', item_number: 13, status: 'Shipped', status_field_name: 'Delivery' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project set-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Delivery', - status_field_id: 'PVTSSF_delivery', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad848', - status: 'Shipped', - previous_status_option_id: 'f75ad847', - previous_status: 'Queued', - changed: true, - }, - }); - assert.equal(itemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - assert.equal(mutationApi.isDone(), true); - assert.equal(observedItemApi.isDone(), true); - assert.equal(observedFieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore can be exercised directly with plain callerName data', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueGetRequest({ issueNumber: 14 }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue get', - input: { issue_number: 14 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue get', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - title: 'Build `orfe` foundation and runtime scaffolding', - body: 'Issue body', - state: 'open', - state_reason: null, - labels: ['needs-input'], - assignees: ['greg'], - html_url: 'https://github.com/throw-if-null/orfe/issues/14', - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue get not-found responses clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueGetRequest({ - issueNumber: 999, - status: 404, - responseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue get', - input: { issue_number: 999 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Issue #999 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue get auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueGetRequest({ - issueNumber: 14, - status: 403, - responseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue get', - input: { issue_number: 14 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while reading issue #14.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore reads a pull request and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetRequest({ prNumber: 9 }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr get', - input: { pr_number: 9 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr get', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: 'issues/orfe-13', - base: 'main', - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr get not-found responses clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetRequest({ - prNumber: 999, - status: 404, - responseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr get', - input: { pr_number: 999 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Pull request #999 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr get auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetRequest({ - prNumber: 9, - status: 403, - responseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr get', - input: { pr_number: 9 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while reading pull request #9.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore reuses an existing pull request for pr get-or-create', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - existingPullRequests: [ - { - number: 9, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - ], - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr get-or-create', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - head: 'issues/orfe-13', - base: 'main', - draft: false, - created: false, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore reuses an existing pull request before validating unused body contract input', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - existingPullRequests: [ - { - number: 9, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - ], - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { - head: 'issues/orfe-13', - title: 'Design the `orfe` custom tool and CLI contract', - body: 'Ref: #13\n\nCloses: #13', - body_contract: 'implementation-ready@1.0.0', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr get-or-create', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - head: 'issues/orfe-13', - base: 'main', - draft: false, - created: false, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore creates a pull request for pr get-or-create when none exists', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - existingPullRequests: [], - createRequestBody: { - head: 'issues/orfe-13', - base: 'main', - title: 'Design the `orfe` custom tool and CLI contract', - body: 'Ref: #13', - draft: true, - }, - createResponseBody: { - number: 10, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'Ref: #13', - state: 'open', - draft: true, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/10', - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { - head: 'issues/orfe-13', - title: 'Design the `orfe` custom tool and CLI contract', - body: 'Ref: #13', - draft: true, - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr get-or-create', - repo: 'throw-if-null/orfe', - data: { - pr_number: 10, - html_url: 'https://github.com/throw-if-null/orfe/pull/10', - head: 'issues/orfe-13', - base: 'main', - draft: true, - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore posts a top-level pull request comment and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestCommentRequest({ prNumber: 9, body: 'Hello from orfe' }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr comment', - input: { pr_number: 9, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr comment', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - comment_id: 123456, - html_url: 'https://github.com/throw-if-null/orfe/pull/9#issuecomment-123456', - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore submits a pull request review and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestSubmitReviewRequest({ prNumber: 9, body: 'Looks good', event: 'APPROVE' }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr submit-review', - input: { pr_number: 9, event: 'approve', body: 'Looks good' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr submit-review', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - review_id: 555, - event: 'approve', - submitted: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects invalid pr submit-review events clearly', async () => { - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr submit-review', - input: { pr_number: 9, event: 'dismiss', body: 'nope' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'invalid_input'); - assert.equal(error.message, 'Review event must be one of: approve, request-changes, comment.'); - return true; - }, - ); -}); - -test('runOrfeCore maps pr submit-review missing pull requests clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestSubmitReviewRequest({ - prNumber: 404, - body: 'Looks good', - event: 'APPROVE', - verifyStatus: 404, - verifyResponseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr submit-review', - input: { pr_number: 404, event: 'approve', body: 'Looks good' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Pull request #404 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), false); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr submit-review auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestSubmitReviewRequest({ - prNumber: 9, - body: 'Looks good', - event: 'APPROVE', - status: 403, - responseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr submit-review', - input: { pr_number: 9, event: 'approve', body: 'Looks good' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while submitting a review on pull request #9.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr submit-review internal failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestSubmitReviewRequest({ - prNumber: 9, - body: 'Looks good', - event: 'APPROVE', - status: 422, - responseBody: { message: 'Validation Failed' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr submit-review', - input: { pr_number: 9, event: 'approve', body: 'Looks good' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'internal_error'); - assert.equal(error.message, 'GitHub API request failed with status 422: Validation Failed'); - assert.equal(error.retryable, false); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr comment not-found responses clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestCommentRequest({ - prNumber: 999, - body: 'Hello from orfe', - status: 404, - responseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr comment', - input: { pr_number: 999, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Pull request #999 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps plain-issue targets for pr comment as not found', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestCommentRequest({ - prNumber: 14, - body: 'Hello from orfe', - verifyStatus: 404, - verifyResponseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr comment', - input: { pr_number: 14, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Pull request #14 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), false); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr comment auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestCommentRequest({ - prNumber: 9, - body: 'Hello from orfe', - status: 403, - responseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr comment', - input: { pr_number: 9, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while commenting on pull request #9.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore surfaces pr comment validation failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestCommentRequest({ - prNumber: 9, - body: 'Hello from orfe', - status: 422, - responseBody: { message: 'Validation Failed' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr comment', - input: { pr_number: 9, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'internal_error'); - assert.equal(error.message, 'GitHub API request failed with status 422: Validation Failed'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore replies to a pull request review comment and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestReplyRequest({ prNumber: 9, commentId: 123456, body: 'ack' }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'pr reply', - input: { pr_number: 9, comment_id: 123456, body: 'ack' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr reply', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - comment_id: 123999, - in_reply_to_comment_id: 123456, - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr reply missing pull requests clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestReplyRequest({ - prNumber: 404, - commentId: 123456, - body: 'ack', - verifyStatus: 404, - verifyResponseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr reply', - input: { pr_number: 404, comment_id: 123456, body: 'ack' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Pull request #404 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), false); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr reply missing review comments clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestReplyRequest({ - prNumber: 9, - commentId: 123456, - body: 'ack', - status: 404, - responseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr reply', - input: { pr_number: 9, comment_id: 123456, body: 'ack' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Review comment #123456 was not found on pull request #9.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr reply auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestReplyRequest({ - prNumber: 9, - commentId: 123456, - body: 'ack', - status: 403, - responseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr reply', - input: { pr_number: 9, comment_id: 123456, body: 'ack' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while replying to review comment #123456 on pull request #9.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects non-repliable pr reply targets clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestReplyRequest({ - prNumber: 9, - commentId: 123456, - body: 'ack', - status: 422, - responseBody: { message: 'Validation Failed' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr reply', - input: { pr_number: 9, comment_id: 123456, body: 'ack' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_conflict'); - assert.equal( - error.message, - 'GitHub rejected reply to review comment #123456 on pull request #9: Validation Failed', - ); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects ambiguous pr get-or-create matches clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - existingPullRequests: [ - { - number: 9, - title: 'First PR', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - { - number: 10, - title: 'Second PR', - body: 'PR body', - state: 'open', - draft: true, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/10', - }, - ], - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_conflict'); - assert.equal( - error.message, - 'Found 2 open pull requests for head "issues/orfe-13" and base "main" in throw-if-null/orfe.', - ); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr get-or-create lookup auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - listStatus: 403, - listResponseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal( - error.message, - 'GitHub App authentication failed while looking up pull requests for head "issues/orfe-13" and base "main".', - ); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps pr get-or-create creation auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - existingPullRequests: [], - createStatus: 403, - createResponseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal( - error.message, - 'GitHub App authentication failed while creating a pull request for head "issues/orfe-13" and base "main".', - ); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore surfaces pr get-or-create creation failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - existingPullRequests: [], - createStatus: 422, - createResponseBody: { message: 'Validation Failed' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'pr get-or-create', - input: { head: 'issues/orfe-13', title: 'Design the `orfe` custom tool and CLI contract' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'internal_error'); - assert.equal(error.message, 'GitHub pull request creation failed with status 422: Validation Failed'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore creates a generic issue and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - body: 'Body text', - labels: ['needs-input'], - assignees: ['greg'], - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - body: 'Body text', - labels: ['needs-input'], - assignees: ['greg'], - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore can create an issue and add it to the default project when explicitly requested', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - const projectLookupApi = mockProjectLookupRequest({ - projectOwner: 'throw-if-null', - projectNumber: 1, - projectId: 'PVT_project_1', - includeAuth: false, - }); - const projectAddApi = mockProjectAddItemRequest({ - projectId: 'PVT_project_1', - contentId: 'I_kwDOOrfeIssue21', - projectItemId: 'PVTI_lAHOABCD1234', - includeAuth: false, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - add_to_project: true, - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - project_assignment: { - project_owner: 'throw-if-null', - project_number: 1, - project_item_id: 'PVTI_lAHOABCD1234', - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(projectLookupApi.isDone(), true); - assert.equal(projectAddApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore can create an issue and add it to an organization-owned project when explicitly requested', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - const projectLookupApi = mockProjectLookupRequest({ - projectOwner: 'throw-if-null', - projectNumber: 1, - projectId: 'PVT_project_org_1', - graphqlResponseBody: createProjectLookupResponse({ projectId: 'PVT_project_org_1', ownerType: 'organization' }), - includeAuth: false, - }); - const projectAddApi = mockProjectAddItemRequest({ - projectId: 'PVT_project_org_1', - contentId: 'I_kwDOOrfeIssue21', - projectItemId: 'PVTI_lAHOABCDORG', - includeAuth: false, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - add_to_project: true, - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - project_assignment: { - project_owner: 'throw-if-null', - project_number: 1, - project_item_id: 'PVTI_lAHOABCDORG', - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(projectLookupApi.isDone(), true); - assert.equal(projectAddApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore can create an issue and add it to a user-owned project when explicitly requested', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - const projectLookupApi = mockProjectLookupRequest({ - projectOwner: 'octocat', - projectNumber: 7, - projectId: 'PVT_project_user_7', - graphqlResponseBody: createProjectLookupResponse({ projectId: 'PVT_project_user_7', ownerType: 'user' }), - includeAuth: false, - }); - const projectAddApi = mockProjectAddItemRequest({ - projectId: 'PVT_project_user_7', - contentId: 'I_kwDOOrfeIssue21', - projectItemId: 'PVTI_lAHOABCDUSER', - includeAuth: false, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - add_to_project: true, - project_owner: 'octocat', - project_number: 7, - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - project_assignment: { - project_owner: 'octocat', - project_number: 7, - project_item_id: 'PVTI_lAHOABCDUSER', - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(projectLookupApi.isDone(), true); - assert.equal(projectAddApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore can create an issue, add it to a project, and set initial status', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - const projectLookupApi = mockProjectLookupRequest({ - projectOwner: 'throw-if-null', - projectNumber: 1, - projectId: 'PVT_project_1', - includeAuth: false, - }); - const projectAddApi = mockProjectAddItemRequest({ - projectId: 'PVT_project_1', - contentId: 'I_kwDOOrfeIssue21', - projectItemId: 'PVTI_lAHOABCD1234', - includeAuth: false, - }); - const currentItemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 21, - includeAuth: false, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - }), - ]), - }, - }, - }, - }, - }); - const fieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - ], - }, - ]), - }, - }, - }, - }); - const mutationApi = mockProjectStatusUpdateRequest({ - projectId: 'PVT_project_1', - itemId: 'PVTI_lAHOABCD1234', - fieldId: 'PVTSSF_lAHOABCD1234', - optionId: 'f75ad845', - }); - const observedItemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 21, - includeAuth: false, - graphqlResponseBody: { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad845', - name: 'Todo', - }), - }), - ]), - }, - }, - }, - }, - }); - const observedFieldsApi = mockProjectStatusFieldsRequest({ - projectId: 'PVT_project_1', - graphqlResponseBody: { - data: { - node: { - fields: createProjectFieldsConnection([ - { - ...createProjectStatusFieldNode({ id: 'PVTSSF_lAHOABCD1234', name: 'Status' }), - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - ], - }, - ]), - }, - }, - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - status: 'Todo', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - project_assignment: { - project_owner: 'throw-if-null', - project_number: 1, - project_item_id: 'PVTI_lAHOABCD1234', - status_field_name: 'Status', - status_option_id: 'f75ad845', - status: 'Todo', - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(projectLookupApi.isDone(), true); - assert.equal(projectAddApi.isDone(), true); - assert.equal(currentItemApi.isDone(), true); - assert.equal(fieldsApi.isDone(), true); - assert.equal(mutationApi.isDone(), true); - assert.equal(observedItemApi.isDone(), true); - assert.equal(observedFieldsApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore preserves create-only behavior when repo config has a default project but no project assignment was requested', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - }); - assert.equal(issueApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore surfaces partial failure details when issue create succeeds but project add fails', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - const projectLookupApi = mockProjectLookupRequest({ - projectOwner: 'throw-if-null', - projectNumber: 1, - projectId: 'PVT_project_1', - includeAuth: false, - }); - const projectAddApi = mockProjectAddItemRequest({ - projectId: 'PVT_project_1', - contentId: 'I_kwDOOrfeIssue21', - graphqlStatus: 403, - graphqlResponseBody: { message: 'Resource not accessible by integration' }, - includeAuth: false, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - add_to_project: true, - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal( - error.message, - 'Issue #21 was created, but adding it to GitHub Project throw-if-null/1 failed: GitHub App authentication failed while adding issue #21 to a GitHub Project.', - ); - assert.deepEqual(error.details, { - stage: 'project_add', - created_issue: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - project_owner: 'throw-if-null', - project_number: 1, - }); - return true; - }, - ); - - assert.equal(issueApi.isDone(), true); - assert.equal(projectLookupApi.isDone(), true); - assert.equal(projectAddApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore reports project_add when status was requested but project add fails', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - const projectLookupApi = mockProjectLookupRequest({ - projectOwner: 'throw-if-null', - projectNumber: 1, - projectId: 'PVT_project_1', - includeAuth: false, - }); - const projectAddApi = mockProjectAddItemRequest({ - projectId: 'PVT_project_1', - contentId: 'I_kwDOOrfeIssue21', - graphqlStatus: 403, - graphqlResponseBody: { message: 'Resource not accessible by integration' }, - includeAuth: false, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - status: 'Todo', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal( - error.message, - 'Issue #21 was created, but adding it to GitHub Project throw-if-null/1 failed: GitHub App authentication failed while adding issue #21 to a GitHub Project.', - ); - assert.deepEqual(error.details, { - stage: 'project_add', - created_issue: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - requested_status: 'Todo', - }); - return true; - }, - ); - - assert.equal(issueApi.isDone(), true); - assert.equal(projectLookupApi.isDone(), true); - assert.equal(projectAddApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore surfaces partial failure details when issue create succeeds but project status fails', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - requestBody: { - title: 'New issue title', - }, - }); - const projectLookupApi = mockProjectLookupRequest({ - projectOwner: 'throw-if-null', - projectNumber: 1, - projectId: 'PVT_project_1', - includeAuth: false, - }); - const projectAddApi = mockProjectAddItemRequest({ - projectId: 'PVT_project_1', - contentId: 'I_kwDOOrfeIssue21', - projectItemId: 'PVTI_lAHOABCD1234', - includeAuth: false, - }); - const currentItemApi = mockProjectGetStatusRequest({ - itemType: 'issue', - itemNumber: 21, - includeAuth: false, - graphqlStatus: 403, - graphqlResponseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { - title: 'New issue title', - status: 'Todo', - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal( - error.message, - 'Issue #21 was created and added to GitHub Project throw-if-null/1, but setting initial status failed: GitHub App authentication failed while setting project status for issue #21.', - ); - assert.deepEqual(error.details, { - stage: 'project_status', - created_issue: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - requested_status: 'Todo', - }); - return true; - }, - ); - - assert.equal(issueApi.isDone(), true); - assert.equal(projectLookupApi.isDone(), true); - assert.equal(projectAddApi.isDone(), true); - assert.equal(currentItemApi.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue create auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCreateRequest({ - requestBody: { title: 'New issue title' }, - status: 403, - responseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { title: 'New issue title' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while creating an issue in throw-if-null/orfe.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue create missing repository failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCreateRequest({ - repo: { owner: 'octo', name: 'missing' }, - requestBody: { title: 'New issue title' }, - status: 404, - responseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { title: 'New issue title', repo: 'octo/missing' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Repository octo/missing was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue create creation failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCreateRequest({ - requestBody: { title: 'New issue title' }, - status: 422, - responseBody: { message: 'Validation Failed' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue create', - input: { title: 'New issue title' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'internal_error'); - assert.equal(error.message, 'GitHub issue creation failed with status 422: Validation Failed'); - assert.equal(error.retryable, false); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore updates issue metadata and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueUpdateRequest({ - issueNumber: 14, - requestBody: { - title: 'Updated title', - body: 'Updated body', - labels: ['bug', 'needs-input'], - assignees: ['greg'], - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue update', - input: { - issue_number: 14, - title: 'Updated title', - body: 'Updated body', - labels: ['bug', 'needs-input'], - assignees: ['greg'], - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue update', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - title: 'Updated title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/14', - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore clears labels and assignees for issue update', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueUpdateRequest({ - issueNumber: 14, - requestBody: { - labels: [], - assignees: [], - }, - responseBody: { - number: 14, - title: 'Build `orfe` foundation and runtime scaffolding', - body: 'Issue body', - state: 'open', - state_reason: null, - labels: [], - assignees: [], - html_url: 'https://github.com/throw-if-null/orfe/issues/14', - }, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue update', - input: { - issue_number: 14, - clear_labels: true, - clear_assignees: true, - }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue update', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - title: 'Build `orfe` foundation and runtime scaffolding', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/14', - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue update not-found responses clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueUpdateRequest({ - issueNumber: 999, - requestBody: { title: 'Updated title' }, - status: 404, - responseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue update', - input: { issue_number: 999, title: 'Updated title' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Issue #999 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue update auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueUpdateRequest({ - issueNumber: 14, - requestBody: { title: 'Updated title' }, - status: 403, - responseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue update', - input: { issue_number: 14, title: 'Updated title' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while updating issue #14.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects pull request targets for issue update clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueUpdateRequest({ - issueNumber: 46, - requestBody: { title: 'Updated title' }, - issueGetResponseBody: { - number: 46, - title: 'Implement `orfe issue update`', - body: 'PR body', - state: 'open', - state_reason: null, - labels: [], - assignees: [], - html_url: 'https://github.com/throw-if-null/orfe/pull/46', - pull_request: { - url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/46', - }, - }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue update', - input: { issue_number: 46, title: 'Updated title' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_conflict'); - assert.equal(error.message, 'Issue #46 is a pull request. issue update only supports issues.'); - return true; - }, - ); - - assert.equal(api.isDone(), false); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore posts a generic issue comment and returns structured success output', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCommentRequest({ issueNumber: 14, body: 'Hello from orfe' }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue comment', - input: { issue_number: 14, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue comment', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - comment_id: 123456, - html_url: 'https://github.com/throw-if-null/orfe/issues/14#issuecomment-123456', - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue comment not-found responses clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCommentRequest({ - issueNumber: 999, - body: 'Hello from orfe', - issueGetStatus: 404, - issueGetResponseBody: { message: 'Not Found' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue comment', - input: { issue_number: 999, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Issue #999 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), false); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue comment auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCommentRequest({ - issueNumber: 14, - body: 'Hello from orfe', - issueGetStatus: 403, - issueGetResponseBody: { message: 'Resource not accessible by integration' }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue comment', - input: { issue_number: 14, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while commenting on issue #14.'); - return true; - }, - ); - - assert.equal(api.isDone(), false); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore closes an issue with structured state metadata', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateRequest({ - issueNumber: 14, - currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), - restUpdateBody: { state: 'closed', state_reason: 'completed' }, - observedIssueState: createIssueStateNode({ - id: 'I_14', - issueNumber: 14, - state: 'CLOSED', - stateReason: 'COMPLETED', - }), - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'completed' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue set-state', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - state: 'closed', - state_reason: 'completed', - duplicate_of_issue_number: null, - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore reopens an issue and clears duplicate metadata', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateRequest({ - issueNumber: 14, - currentIssueState: createIssueStateNode({ - id: 'I_14', - issueNumber: 14, - state: 'CLOSED', - stateReason: 'DUPLICATE', - duplicateOfIssueNumber: 7, - duplicateOfId: 'I_7', - }), - unmark: { duplicateId: 'I_14', canonicalId: 'I_7' }, - restUpdateBody: { state: 'open' }, - observedIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'open' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue set-state', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - state: 'open', - state_reason: null, - duplicate_of_issue_number: null, - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore closes an issue as a duplicate and returns canonical issue metadata', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateDuplicateRequest({ - issueNumber: 14, - duplicateOfIssueNumber: 7, - currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), - canonicalIssueState: createIssueStateNode({ id: 'I_7', issueNumber: 7, state: 'OPEN' }), - mark: { duplicateId: 'I_14', canonicalId: 'I_7' }, - observedIssueState: createIssueStateNode({ - id: 'I_14', - issueNumber: 14, - state: 'CLOSED', - stateReason: 'DUPLICATE', - duplicateOfIssueNumber: 7, - duplicateOfId: 'I_7', - }), - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 7 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue set-state', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - state: 'closed', - state_reason: 'duplicate', - duplicate_of_issue_number: 7, - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore treats matching issue set-state requests as no-ops', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateRequest({ - issueNumber: 14, - currentIssueState: createIssueStateNode({ - id: 'I_14', - issueNumber: 14, - state: 'CLOSED', - stateReason: 'COMPLETED', - }), - includeGraphql: true, - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'completed' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue set-state', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - state: 'closed', - state_reason: 'completed', - duplicate_of_issue_number: null, - changed: false, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue set-state missing duplicate target clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateDuplicateRequest({ - issueNumber: 14, - duplicateOfIssueNumber: 999, - currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), - canonicalIssueState: null, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 999 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_not_found'); - assert.equal(error.message, 'Duplicate target issue #999 was not found.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects pull request duplicate targets for issue set-state clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateDuplicateRequest({ - issueNumber: 14, - duplicateOfIssueNumber: 48, - currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), - canonicalIssueState: null, - duplicateTargetGetStatus: 200, - duplicateTargetGetResponseBody: createIssueRestResponse(48, { - title: 'Implement `orfe issue set-state`', - html_url: 'https://github.com/throw-if-null/orfe/pull/48', - pull_request: { - url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/48', - }, - }), - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 48 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_conflict'); - assert.equal(error.message, 'Duplicate target issue #48 is a pull request. --duplicate-of must reference an issue.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore maps issue set-state auth failures clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateRequest({ - issueNumber: 14, - currentIssueState: createIssueStateNode({ id: 'I_14', issueNumber: 14, state: 'OPEN' }), - issueGetStatus: 403, - issueGetResponseBody: { message: 'Resource not accessible by integration' }, - includeGraphql: false, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'completed' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'auth_failed'); - assert.equal(error.message, 'GitHub App authentication failed while setting state for issue #14.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects pull request targets for issue set-state clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateRequest({ - issueNumber: 46, - currentIssueState: createIssueStateNode({ id: 'I_46', issueNumber: 46, state: 'OPEN' }), - issueGetResponseBody: { - number: 46, - title: 'Implement `orfe issue set-state`', - body: 'PR body', - state: 'open', - state_reason: null, - labels: [], - assignees: [], - html_url: 'https://github.com/throw-if-null/orfe/pull/46', - pull_request: { - url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/46', - }, - }, - includeGraphql: false, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 46, state: 'closed', state_reason: 'completed' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_conflict'); - assert.equal(error.message, 'Issue #46 is a pull request. issue set-state only supports issues.'); - return true; - }, - ); - - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore re-targets duplicate issue set-state requests and reports changes', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateDuplicateRequest({ - issueNumber: 14, - duplicateOfIssueNumber: 9, - currentIssueState: createIssueStateNode({ - id: 'I_14', - issueNumber: 14, - state: 'CLOSED', - stateReason: 'DUPLICATE', - duplicateOfIssueNumber: 7, - duplicateOfId: 'I_7', - }), - canonicalIssueState: createIssueStateNode({ id: 'I_9', issueNumber: 9, state: 'OPEN' }), - unmark: { duplicateId: 'I_14', canonicalId: 'I_7' }, - mark: { duplicateId: 'I_14', canonicalId: 'I_9' }, - observedIssueState: createIssueStateNode({ - id: 'I_14', - issueNumber: 14, - state: 'CLOSED', - stateReason: 'DUPLICATE', - duplicateOfIssueNumber: 9, - duplicateOfId: 'I_9', - }), - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 9 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue set-state', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - state: 'closed', - state_reason: 'duplicate', - duplicate_of_issue_number: 9, - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore treats matching duplicate issue set-state requests as no-ops', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueSetStateDuplicateRequest({ - issueNumber: 14, - duplicateOfIssueNumber: 7, - currentIssueState: createIssueStateNode({ - id: 'I_14', - issueNumber: 14, - state: 'CLOSED', - stateReason: 'DUPLICATE', - duplicateOfIssueNumber: 7, - duplicateOfId: 'I_7', - }), - canonicalIssueState: createIssueStateNode({ id: 'I_7', issueNumber: 7, state: 'OPEN' }), - }); - - const result = await runOrfeCore( - { - callerName: 'Greg', - command: 'issue set-state', - input: { issue_number: 14, state: 'closed', state_reason: 'duplicate', duplicate_of: 7 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue set-state', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - state: 'closed', - state_reason: 'duplicate', - duplicate_of_issue_number: 7, - changed: false, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects pull request targets for issue comment clearly', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCommentRequest({ - issueNumber: 46, - body: 'Hello from orfe', - issueGetResponseBody: { - number: 46, - title: 'Implement `orfe issue comment`', - body: 'PR body', - state: 'open', - state_reason: null, - labels: [], - assignees: [], - html_url: 'https://github.com/throw-if-null/orfe/pull/46', - pull_request: { - url: 'https://api.github.com/repos/throw-if-null/orfe/pulls/46', - }, - }, - }); - - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue comment', - input: { issue_number: 46, body: 'Hello from orfe' }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'github_conflict'); - assert.equal(error.message, 'Issue #46 is a pull request. Use pr comment instead.'); - return true; - }, - ); - - assert.equal(api.isDone(), false); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('runOrfeCore rejects unmapped callers clearly', async () => { - await assert.rejects( - runOrfeCore( - { - callerName: 'Unknown Agent', - command: 'issue get', - input: { issue_number: 14 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'caller_name_unmapped'); - assert.match(error.message, /Caller name "Unknown Agent" is not mapped/); - return true; - }, - ); -}); - -test('runOrfeCore rejects empty caller names clearly', async () => { - await assert.rejects( - runOrfeCore( - { - callerName: ' ', - command: 'issue get', - input: { issue_number: 14 }, - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'caller_name_missing'); - return true; - }, - ); -}); - -test('runOrfeCore rejects repo-local config failures before auth config loading succeeds', async () => { - await assert.rejects( - runOrfeCore( - { - callerName: 'Greg', - command: 'issue get', - input: { issue_number: 14 }, - }, - { - loadRepoConfigImpl: async () => { - throw new OrfeError('config_not_found', 'repo-local config not found at /tmp/.orfe/config.json.'); - }, - loadAuthConfigImpl: async () => { - throw new OrfeError('internal_error', 'auth config should not load'); - }, - }, - ), - (error: unknown) => { - assert(error instanceof OrfeError); - assert.equal(error.code, 'config_not_found'); - return true; - }, - ); -}); diff --git a/test/core/auth-config-loading.test.ts b/test/core/auth-config-loading.test.ts new file mode 100644 index 0000000..00dbf15 --- /dev/null +++ b/test/core/auth-config-loading.test.ts @@ -0,0 +1,135 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../src/errors.js'; +import { runOrfeCore } from '../../src/core.js'; +import { createAuthConfig, createRepoConfig } from '../support/runtime-fixtures.js'; + +test('runOrfeCore rejects unmapped callers clearly for GitHub-backed commands', async () => { + await assert.rejects( + runOrfeCore( + { + callerName: 'Unknown Agent', + command: 'issue get', + input: { issue_number: 14 }, + }, + { + loadRepoConfigImpl: async () => createRepoConfig(), + loadAuthConfigImpl: async () => createAuthConfig(), + }, + ), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'caller_name_unmapped'); + assert.match(error.message, /Caller name "Unknown Agent" is not mapped/); + return true; + }, + ); +}); + +test('runOrfeCore surfaces auth config loading failures clearly for GitHub-backed commands', async () => { + await assert.rejects( + runOrfeCore( + { + callerName: 'Greg', + command: 'issue get', + input: { issue_number: 14 }, + }, + { + loadRepoConfigImpl: async () => createRepoConfig(), + loadAuthConfigImpl: async () => { + throw new OrfeError('config_not_found', 'machine-local auth config not found at /tmp/auth.json.'); + }, + }, + ), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'config_not_found'); + assert.equal(error.message, 'machine-local auth config not found at /tmp/auth.json.'); + return true; + }, + ); +}); + +test('runOrfeCore forwards explicit auth config paths into shared auth config loading', async () => { + let capturedOptions: Record | undefined; + + await assert.rejects( + runOrfeCore( + { + callerName: 'Greg', + command: 'issue get', + input: { issue_number: 14 }, + cwd: '/tmp/repo', + authConfigPath: '/tmp/custom-auth.json', + }, + { + loadRepoConfigImpl: async () => createRepoConfig(), + loadAuthConfigImpl: async (options = {}) => { + capturedOptions = { + ...(options.cwd ? { cwd: options.cwd } : {}), + ...(options.authConfigPath ? { authConfigPath: options.authConfigPath } : {}), + }; + + throw new OrfeError('config_not_found', 'machine-local auth config not found at /tmp/custom-auth.json.'); + }, + }, + ), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'config_not_found'); + assert.equal(error.message, 'machine-local auth config not found at /tmp/custom-auth.json.'); + return true; + }, + ); + + assert.deepEqual(capturedOptions, { + cwd: '/tmp/repo', + authConfigPath: '/tmp/custom-auth.json', + }); +}); + +test('runOrfeCore rejects empty caller names clearly', async () => { + await assert.rejects( + runOrfeCore( + { + callerName: ' ', + command: 'issue get', + input: { issue_number: 14 }, + }, + { + loadRepoConfigImpl: async () => createRepoConfig(), + }, + ), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'caller_name_missing'); + return true; + }, + ); +}); + +test('runOrfeCore rejects repo-local config failures before auth config loading succeeds', async () => { + await assert.rejects( + runOrfeCore( + { + callerName: 'Greg', + command: 'issue get', + input: { issue_number: 14 }, + }, + { + loadRepoConfigImpl: async () => { + throw new OrfeError('config_not_found', 'repo-local config not found at /tmp/.orfe/config.json.'); + }, + loadAuthConfigImpl: async () => { + throw new OrfeError('internal_error', 'auth config should not load'); + }, + }, + ), + (error: unknown) => { + assert(error instanceof OrfeError); + assert.equal(error.code, 'config_not_found'); + return true; + }, + ); +}); diff --git a/test/core/runtime-routing.test.ts b/test/core/runtime-routing.test.ts new file mode 100644 index 0000000..f9564ba --- /dev/null +++ b/test/core/runtime-routing.test.ts @@ -0,0 +1,108 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { COMMANDS } from '../../src/commands/index.js'; +import { createHelpCommandSuccessData, createHelpRootSuccessData } from '../../src/commands/help/definition.js'; +import { runOrfeCore } from '../../src/core.js'; + +test('runOrfeCore returns runtime info without caller, config, auth, or GitHub access', async () => { + const result = await runOrfeCore( + { + callerName: '', + command: 'runtime info', + input: {}, + entrypoint: 'cli', + }, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + assert.equal(result.command, 'runtime info'); + assert.equal(result.repo, undefined); + const data = result.data as { orfe_version: string; entrypoint: string }; + assert.match(data.orfe_version, /^\d+\.\d+\.\d+/); + assert.deepEqual(data, { + orfe_version: data.orfe_version, + entrypoint: 'cli', + }); +}); + +test('runOrfeCore returns root help without caller, config, auth, or GitHub access', async () => { + const result = await runOrfeCore( + { + callerName: '', + command: 'help', + input: {}, + entrypoint: 'opencode-plugin', + }, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + assert.equal(result.command, 'help'); + assert.equal(result.repo, undefined); + assert.deepEqual(result.data, createHelpRootSuccessData(COMMANDS)); +}); + +test('runOrfeCore returns targeted command help without caller, config, auth, or GitHub access', async () => { + const result = await runOrfeCore( + { + callerName: '', + command: 'help', + input: { command_name: 'runtime info' }, + entrypoint: 'opencode-plugin', + }, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + assert.equal(result.command, 'help'); + assert.equal(result.repo, undefined); + assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, 'runtime info')); +}); + +test('runOrfeCore returns targeted command help with explicit requirements for representative commands', async () => { + for (const commandName of ['issue get', 'pr get-or-create', 'project set-status'] as const) { + const result = await runOrfeCore( + { + callerName: '', + command: 'help', + input: { command_name: commandName }, + entrypoint: 'opencode-plugin', + }, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + assert.equal(result.command, 'help'); + assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, commandName)); + } +}); diff --git a/test/issue/fixtures.ts b/test/issue/fixtures.ts new file mode 100644 index 0000000..12e69cd --- /dev/null +++ b/test/issue/fixtures.ts @@ -0,0 +1,328 @@ +import nock from 'nock'; + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function matchesIssueStateLookup(body: unknown, issueNumber: number): boolean { + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes('query IssueStateByNumber') && + isObject(body.variables) && + body.variables.issueNumber === issueNumber + ); +} + +function matchesMarkIssueAsDuplicate(body: unknown, duplicateId: string, canonicalId: string): boolean { + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes('mutation MarkIssueAsDuplicate') && + isObject(body.variables) && + body.variables.duplicateId === duplicateId && + body.variables.canonicalId === canonicalId + ); +} + +function matchesUnmarkIssueAsDuplicate(body: unknown, duplicateId: string, canonicalId: string): boolean { + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes('mutation UnmarkIssueAsDuplicate') && + isObject(body.variables) && + body.variables.duplicateId === duplicateId && + body.variables.canonicalId === canonicalId + ); +} + +export function createIssueRestResponse(issueNumber: number, overrides: Record = {}) { + return { + number: issueNumber, + title: 'Issue title', + body: 'Issue body', + state: 'open', + state_reason: null, + labels: [], + assignees: [], + html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, + ...overrides, + }; +} + +export function createIssueStateNode(options: { + id: string; + issueNumber: number; + state: string; + stateReason?: string | null; + duplicateOfIssueNumber?: number; + duplicateOfId?: string; +}) { + return { + id: options.id, + number: options.issueNumber, + state: options.state, + stateReason: options.stateReason ?? null, + duplicateOf: + options.duplicateOfIssueNumber !== undefined + ? { + id: options.duplicateOfId ?? `I_${options.duplicateOfIssueNumber}`, + number: options.duplicateOfIssueNumber, + } + : null, + }; +} + +export function mockIssueGetRequest(options: { + issueNumber: number; + status?: number; + responseBody?: Record; +}) { + const issueNumber = options.issueNumber; + const status = options.status ?? 200; + + return nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) + .reply( + status, + options.responseBody ?? { + number: issueNumber, + title: 'Build `orfe` foundation and runtime scaffolding', + body: 'Issue body', + state: 'open', + state_reason: null, + labels: [{ name: 'needs-input' }], + assignees: [{ login: 'greg' }], + html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, + }, + ); +} + +export function mockIssueCreateRequest(options: { + requestBody: Record; + status?: number; + responseBody?: Record; + repo?: { owner: string; name: string }; +}) { + const owner = options.repo?.owner ?? 'throw-if-null'; + const repo = options.repo?.name ?? 'orfe'; + const status = options.status ?? 201; + + return nock('https://api.github.com') + .get(`/repos/${owner}/${repo}/installation`) + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .post(`/repos/${owner}/${repo}/issues`, (body: unknown) => JSON.stringify(body) === JSON.stringify(options.requestBody)) + .reply( + status, + options.responseBody ?? { + number: 21, + node_id: 'I_kwDOOrfeIssue21', + title: options.requestBody.title, + body: options.requestBody.body ?? '', + state: 'open', + state_reason: null, + labels: ((options.requestBody.labels as string[] | undefined) ?? []).map((name) => ({ name })), + assignees: ((options.requestBody.assignees as string[] | undefined) ?? []).map((login) => ({ login })), + html_url: `https://github.com/${owner}/${repo}/issues/21`, + }, + ); +} + +export function mockIssueUpdateRequest(options: { + issueNumber: number; + requestBody: Record; + status?: number; + responseBody?: Record; + issueGetStatus?: number; + issueGetResponseBody?: Record; +}) { + const issueNumber = options.issueNumber; + const status = options.status ?? 200; + const issueGetStatus = options.issueGetStatus ?? 200; + + return nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) + .reply( + issueGetStatus, + options.issueGetResponseBody ?? { + number: issueNumber, + title: 'Updated title', + body: 'Updated body', + state: 'open', + state_reason: null, + labels: [{ name: 'bug' }, { name: 'needs-input' }], + assignees: [{ login: 'greg' }], + html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, + }, + ) + .patch(`/repos/throw-if-null/orfe/issues/${issueNumber}`, (body: unknown) => JSON.stringify(body) === JSON.stringify(options.requestBody)) + .reply( + status, + options.responseBody ?? { + number: issueNumber, + title: 'Updated title', + body: 'Updated body', + state: 'open', + state_reason: null, + labels: [{ name: 'bug' }, { name: 'needs-input' }], + assignees: [{ login: 'greg' }], + html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, + }, + ); +} + +export function mockIssueCommentRequest(options: { + issueNumber: number; + body: string; + status?: number; + responseBody?: Record; + issueGetStatus?: number; + issueGetResponseBody?: Record; +}) { + const issueNumber = options.issueNumber; + const status = options.status ?? 201; + const issueGetStatus = options.issueGetStatus ?? 200; + + return nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) + .reply( + issueGetStatus, + options.issueGetResponseBody ?? { + number: issueNumber, + title: 'Issue title', + body: 'Issue body', + state: 'open', + state_reason: null, + labels: [], + assignees: [], + html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, + }, + ) + .post(`/repos/throw-if-null/orfe/issues/${issueNumber}/comments`, (body: unknown) => JSON.stringify(body) === JSON.stringify({ body: options.body })) + .reply( + status, + options.responseBody ?? { + id: 123456, + html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}#issuecomment-123456`, + }, + ); +} + +export function mockIssueSetStateRequest(options: { + issueNumber: number; + currentIssueState: Record; + restUpdateBody?: Record; + observedIssueState?: Record; + issueGetStatus?: number; + issueGetResponseBody?: Record; + includeGraphql?: boolean; + unmark?: { duplicateId: string; canonicalId: string }; +}) { + const scope = nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`) + .reply(options.issueGetStatus ?? 200, options.issueGetResponseBody ?? createIssueRestResponse(options.issueNumber)); + + if (options.includeGraphql !== false) { + scope.post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)).reply(200, { + data: { repository: { issue: options.currentIssueState } }, + }); + } + + if (options.unmark) { + scope + .post('/graphql', (body: unknown) => matchesUnmarkIssueAsDuplicate(body, options.unmark!.duplicateId, options.unmark!.canonicalId)) + .reply(200, { data: { unmarkIssueAsDuplicate: { clientMutationId: null } } }); + } + + if (options.restUpdateBody) { + scope + .patch(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`, (body: unknown) => JSON.stringify(body) === JSON.stringify(options.restUpdateBody)) + .reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)) + .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)) + .reply(200, { data: { repository: { issue: options.observedIssueState ?? options.currentIssueState } } }); + } + + return scope; +} + +export function mockIssueSetStateDuplicateRequest(options: { + issueNumber: number; + duplicateOfIssueNumber: number; + currentIssueState: Record; + canonicalIssueState: Record | null; + duplicateTargetGetStatus?: number; + duplicateTargetGetResponseBody?: Record; + unmark?: { duplicateId: string; canonicalId: string }; + mark?: { duplicateId: string; canonicalId: string }; + restUpdateBody?: Record; + observedIssueState?: Record; + issueGetStatus?: number; + issueGetResponseBody?: Record; + includeGraphql?: boolean; +}) { + const scope = nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`) + .reply(options.issueGetStatus ?? 200, options.issueGetResponseBody ?? createIssueRestResponse(options.issueNumber)); + + if (options.includeGraphql !== false) { + scope + .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)) + .reply(200, { data: { repository: { issue: options.currentIssueState } } }) + .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.duplicateOfIssueNumber)) + .reply(200, { data: { repository: { issue: options.canonicalIssueState } } }); + } + + if (options.canonicalIssueState === null) { + scope + .get(`/repos/throw-if-null/orfe/issues/${options.duplicateOfIssueNumber}`) + .reply(options.duplicateTargetGetStatus ?? 404, options.duplicateTargetGetResponseBody ?? { message: 'Not Found' }); + } + + if (options.unmark) { + scope + .post('/graphql', (body: unknown) => matchesUnmarkIssueAsDuplicate(body, options.unmark!.duplicateId, options.unmark!.canonicalId)) + .reply(200, { data: { unmarkIssueAsDuplicate: { clientMutationId: null } } }); + } + + if (options.mark) { + scope + .post('/graphql', (body: unknown) => matchesMarkIssueAsDuplicate(body, options.mark!.duplicateId, options.mark!.canonicalId)) + .reply(200, { data: { markIssueAsDuplicate: { clientMutationId: null } } }); + } + + if (options.observedIssueState) { + if (options.restUpdateBody) { + scope + .patch(`/repos/throw-if-null/orfe/issues/${options.issueNumber}`, (body: unknown) => JSON.stringify(body) === JSON.stringify(options.restUpdateBody)) + .reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)); + } + + scope + .post('/graphql', (body: unknown) => matchesIssueStateLookup(body, options.issueNumber)) + .reply(200, { data: { repository: { issue: options.observedIssueState } } }); + } + + return scope; +} diff --git a/test/pr/fixtures.ts b/test/pr/fixtures.ts new file mode 100644 index 0000000..ebf14a7 --- /dev/null +++ b/test/pr/fixtures.ts @@ -0,0 +1,210 @@ +import nock from 'nock'; + +export function mockPullRequestGetRequest(options: { + prNumber: number; + status?: number; + responseBody?: Record; +}) { + const prNumber = options.prNumber; + const status = options.status ?? 200; + + return nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) + .reply( + status, + options.responseBody ?? { + number: prNumber, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, + }, + ); +} + +export function mockPullRequestGetOrCreateRequest(options: { + head: string; + base?: string; + existingPullRequests?: Record[]; + listStatus?: number; + listResponseBody?: unknown; + createRequestBody?: Record; + createStatus?: number; + createResponseBody?: Record; +}) { + const head = options.head; + const base = options.base ?? 'main'; + const scope = nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get('/repos/throw-if-null/orfe/pulls') + .query({ state: 'open', head: `throw-if-null:${head}`, base, per_page: 100 }) + .reply(options.listStatus ?? 200, options.listResponseBody ?? options.existingPullRequests ?? []); + + if (options.createStatus !== undefined || options.createResponseBody !== undefined || options.createRequestBody !== undefined) { + scope + .post('/repos/throw-if-null/orfe/pulls', (body: unknown) => + JSON.stringify(body) === + JSON.stringify( + options.createRequestBody ?? { + head, + base, + title: 'Design the `orfe` custom tool and CLI contract', + draft: false, + }, + ), + ) + .reply( + options.createStatus ?? 201, + options.createResponseBody ?? { + number: 9, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: head }, + base: { ref: base }, + html_url: 'https://github.com/throw-if-null/orfe/pull/9', + }, + ); + } + + return scope; +} + +export function mockPullRequestCommentRequest(options: { + prNumber: number; + body: string; + verifyStatus?: number; + verifyResponseBody?: Record; + status?: number; + responseBody?: Record; +}) { + const prNumber = options.prNumber; + const verifyStatus = options.verifyStatus ?? 200; + const status = options.status ?? 201; + + return nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) + .reply( + verifyStatus, + options.verifyResponseBody ?? { + number: prNumber, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, + }, + ) + .post(`/repos/throw-if-null/orfe/issues/${prNumber}/comments`, { body: options.body }) + .reply( + status, + options.responseBody ?? { + id: 123456, + html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}#issuecomment-123456`, + }, + ); +} + +export function mockPullRequestReplyRequest(options: { + prNumber: number; + commentId: number; + body: string; + verifyStatus?: number; + verifyResponseBody?: Record; + status?: number; + responseBody?: Record; +}) { + const prNumber = options.prNumber; + const commentId = options.commentId; + const verifyStatus = options.verifyStatus ?? 200; + const status = options.status ?? 201; + + return nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) + .reply( + verifyStatus, + options.verifyResponseBody ?? { + number: prNumber, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, + }, + ) + .post(`/repos/throw-if-null/orfe/pulls/${prNumber}/comments/${commentId}/replies`, { body: options.body }) + .reply( + status, + options.responseBody ?? { + id: 123999, + in_reply_to_id: commentId, + }, + ); +} + +export function mockPullRequestSubmitReviewRequest(options: { + prNumber: number; + body: string; + event: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT'; + verifyStatus?: number; + verifyResponseBody?: Record; + status?: number; + responseBody?: Record; +}) { + const prNumber = options.prNumber; + const verifyStatus = options.verifyStatus ?? 200; + const status = options.status ?? 200; + + return nock('https://api.github.com') + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) + .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) + .reply( + verifyStatus, + options.verifyResponseBody ?? { + number: prNumber, + title: 'Design the `orfe` custom tool and CLI contract', + body: 'PR body', + state: 'open', + draft: false, + head: { ref: 'issues/orfe-13' }, + base: { ref: 'main' }, + html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, + }, + ) + .post(`/repos/throw-if-null/orfe/pulls/${prNumber}/reviews`, { + body: options.body, + event: options.event, + }) + .reply( + status, + options.responseBody ?? { + id: 555, + }, + ); +} diff --git a/test/project/fixtures.ts b/test/project/fixtures.ts new file mode 100644 index 0000000..fc5d102 --- /dev/null +++ b/test/project/fixtures.ts @@ -0,0 +1,319 @@ +import nock from 'nock'; + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function matchesProjectByOwnerAndNumber(body: unknown, options: { projectOwner: string; projectNumber: number }): boolean { + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes('query ProjectByOwnerAndNumber') && + isObject(body.variables) && + body.variables.login === options.projectOwner && + body.variables.number === options.projectNumber + ); +} + +function matchesProjectAddItem(body: unknown, options: { projectId: string; contentId: string }): boolean { + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes('mutation AddProjectItem') && + isObject(body.variables) && + body.variables.projectId === options.projectId && + body.variables.contentId === options.contentId + ); +} + +function matchesProjectStatusLookup(body: unknown, options: { itemType: 'issue' | 'pr'; itemNumber: number; statusFieldName: string }): boolean { + const expectedQueryName = options.itemType === 'issue' ? 'query ProjectStatusForIssue' : 'query ProjectStatusForPullRequest'; + + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes(expectedQueryName) && + isObject(body.variables) && + body.variables.itemNumber === options.itemNumber && + body.variables.statusFieldName === options.statusFieldName + ); +} + +function matchesProjectStatusFields(body: unknown, options: { projectId: string; fieldsCursor?: string | null }): boolean { + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes('query ProjectStatusFields') && + isObject(body.variables) && + body.variables.projectId === options.projectId && + body.variables.fieldsCursor === (options.fieldsCursor ?? null) + ); +} + +function matchesProjectStatusUpdate(body: unknown, options: { projectId: string; itemId: string; fieldId: string; optionId: string }): boolean { + return ( + isObject(body) && + typeof body.query === 'string' && + body.query.includes('mutation UpdateProjectStatus') && + isObject(body.variables) && + body.variables.projectId === options.projectId && + body.variables.itemId === options.itemId && + body.variables.fieldId === options.fieldId && + body.variables.optionId === options.optionId + ); +} + +function createPageInfo(options?: { hasNextPage?: boolean; endCursor?: string | null }) { + return { + hasNextPage: options?.hasNextPage ?? false, + endCursor: options?.endCursor ?? null, + }; +} + +export function createProjectLookupResponse(options: { projectId: string; ownerType?: 'organization' | 'user' }) { + return { + data: { + repositoryOwner: { + __typename: options.ownerType === 'user' ? 'User' : 'Organization', + projectV2: { + id: options.projectId, + }, + }, + }, + }; +} + +export function createProjectItemsConnection(nodes: unknown[], options?: { hasNextPage?: boolean; endCursor?: string | null }) { + return { + nodes, + pageInfo: createPageInfo(options), + }; +} + +export function createProjectFieldsConnection(nodes: unknown[], options?: { hasNextPage?: boolean; endCursor?: string | null }) { + return { + nodes, + pageInfo: createPageInfo(options), + }; +} + +export function createProjectStatusFieldNode(options: { id: string; name: string; options?: Array<{ id: string; name: string }> }) { + return { + __typename: 'ProjectV2SingleSelectField', + id: options.id, + name: options.name, + ...(options.options ? { options: options.options } : {}), + }; +} + +export function createProjectStatusValueNode(options: { fieldId: string; fieldName: string; optionId: string; name: string }) { + return { + __typename: 'ProjectV2ItemFieldSingleSelectValue', + optionId: options.optionId, + name: options.name, + field: { + __typename: 'ProjectV2SingleSelectField', + id: options.fieldId, + name: options.fieldName, + }, + }; +} + +export function createProjectItemNode(options: { + id: string; + projectId?: string; + projectOwner: string; + projectNumber: number; + fields?: unknown[]; + statusValue?: unknown; +}) { + return { + id: options.id, + project: { + id: options.projectId ?? 'PVT_project_1', + number: options.projectNumber, + owner: { + login: options.projectOwner, + }, + ...(options.fields + ? { + fields: { + nodes: options.fields, + }, + } + : {}), + }, + fieldValueByName: options.statusValue ?? null, + }; +} + +export function mockProjectGetStatusRequest(options: { + itemType: 'issue' | 'pr'; + itemNumber: number; + statusFieldName?: string; + graphqlStatus?: number; + graphqlResponseBody?: Record; + projectItemsCursor?: string | null; + includeAuth?: boolean; +}) { + const statusFieldName = options.statusFieldName ?? 'Status'; + + let scope = nock('https://api.github.com'); + if (options.includeAuth !== false) { + scope = scope + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }); + } + + return scope + .post('/graphql', (body: unknown) => + matchesProjectStatusLookup(body, { + itemType: options.itemType, + itemNumber: options.itemNumber, + statusFieldName, + }) && isObject(body) && isObject(body.variables) && body.variables.projectItemsCursor === (options.projectItemsCursor ?? null), + ) + .reply( + options.graphqlStatus ?? 200, + options.graphqlResponseBody ?? { + data: { + repository: + options.itemType === 'issue' + ? { + issue: { + projectItems: createProjectItemsConnection([]), + }, + } + : { + pullRequest: { + projectItems: createProjectItemsConnection([]), + }, + }, + }, + }, + ); +} + +export function mockProjectStatusFieldsRequest(options: { + projectId?: string; + fieldsCursor?: string | null; + graphqlStatus?: number; + graphqlResponseBody?: Record; +}) { + return nock('https://api.github.com') + .post('/graphql', (body: unknown) => + matchesProjectStatusFields(body, { + projectId: options.projectId ?? 'PVT_project_1', + ...(options.fieldsCursor !== undefined ? { fieldsCursor: options.fieldsCursor } : {}), + }), + ) + .reply( + options.graphqlStatus ?? 200, + options.graphqlResponseBody ?? { + data: { + node: { + fields: createProjectFieldsConnection([]), + }, + }, + }, + ); +} + +export function mockProjectStatusUpdateRequest(options: { + projectId?: string; + itemId: string; + fieldId: string; + optionId: string; + graphqlStatus?: number; + graphqlResponseBody?: Record; +}) { + return nock('https://api.github.com') + .post('/graphql', (body: unknown) => + matchesProjectStatusUpdate(body, { + projectId: options.projectId ?? 'PVT_project_1', + itemId: options.itemId, + fieldId: options.fieldId, + optionId: options.optionId, + }), + ) + .reply( + options.graphqlStatus ?? 200, + options.graphqlResponseBody ?? { + data: { + updateProjectV2ItemFieldValue: { + clientMutationId: null, + }, + }, + }, + ); +} + +export function mockProjectLookupRequest(options: { + projectOwner: string; + projectNumber: number; + projectId?: string; + graphqlStatus?: number; + graphqlResponseBody?: Record; + includeAuth?: boolean; +}) { + let scope = nock('https://api.github.com'); + if (options.includeAuth !== false) { + scope = scope + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }); + } + + return scope + .post('/graphql', (body: unknown) => + matchesProjectByOwnerAndNumber(body, { + projectOwner: options.projectOwner, + projectNumber: options.projectNumber, + }), + ) + .reply( + options.graphqlStatus ?? 200, + options.graphqlResponseBody ?? createProjectLookupResponse({ projectId: options.projectId ?? 'PVT_project_1' }), + ); +} + +export function mockProjectAddItemRequest(options: { + projectId?: string; + contentId: string; + projectItemId?: string; + graphqlStatus?: number; + graphqlResponseBody?: Record; + includeAuth?: boolean; +}) { + let scope = nock('https://api.github.com'); + if (options.includeAuth !== false) { + scope = scope + .get('/repos/throw-if-null/orfe/installation') + .reply(200, { id: 42 }) + .post('/app/installations/42/access_tokens') + .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }); + } + + return scope + .post('/graphql', (body: unknown) => + matchesProjectAddItem(body, { + projectId: options.projectId ?? 'PVT_project_1', + contentId: options.contentId, + }), + ) + .reply( + options.graphqlStatus ?? 200, + options.graphqlResponseBody ?? { + data: { + addProjectV2ItemById: { + item: { + id: options.projectItemId ?? 'PVTI_lAHOABCD1234', + }, + }, + }, + }, + ); +} diff --git a/test/support/command-runtime.ts b/test/support/command-runtime.ts new file mode 100644 index 0000000..d88727f --- /dev/null +++ b/test/support/command-runtime.ts @@ -0,0 +1,85 @@ +import { runOrfeCore, type OrfeCoreDependencies } from '../../src/core.js'; +import { executeOrfeTool, type OpenCodeToolContext, type OrfeToolDependencies } from '../../src/wrapper.js'; +import type { CommandInput, OrfeCoreRequest } from '../../src/types.js'; + +import { + createAuthConfig, + createGitHubClientFactory, + createRepoConfig, + createRepoConfigWithDefaultProject, +} from './runtime-fixtures.js'; + +export function createCoreDependencies(options: { + repoConfig?: ReturnType; + authConfig?: ReturnType; + overrides?: OrfeCoreDependencies; +} = {}): OrfeCoreDependencies { + return { + loadRepoConfigImpl: async () => options.repoConfig ?? createRepoConfig(), + loadAuthConfigImpl: async () => options.authConfig ?? createAuthConfig(), + githubClientFactory: createGitHubClientFactory(), + ...options.overrides, + }; +} + +export async function runCoreCommand(options: { + command: string; + input: CommandInput; + request?: Partial; + dependencies?: OrfeCoreDependencies; + repoConfig?: ReturnType; + authConfig?: ReturnType; +}) { + const fallbackDependencies = createCoreDependencies({ + ...(options.repoConfig ? { repoConfig: options.repoConfig } : {}), + ...(options.authConfig ? { authConfig: options.authConfig } : {}), + }); + + return runOrfeCore( + { + callerName: 'Greg', + command: options.command, + input: options.input, + ...options.request, + }, + options.dependencies ?? fallbackDependencies, + ); +} + +export function createToolDependencies(options: { + repoConfig?: ReturnType; + authConfig?: ReturnType; + overrides?: OrfeToolDependencies; +} = {}): OrfeToolDependencies { + return { + loadRepoConfigImpl: async () => options.repoConfig ?? createRepoConfig(), + loadAuthConfigImpl: async () => options.authConfig ?? createAuthConfig(), + githubClientFactory: createGitHubClientFactory(), + ...options.overrides, + }; +} + +export async function runToolCommand(options: { + input: Record; + context?: OpenCodeToolContext; + dependencies?: OrfeToolDependencies; + repoConfig?: ReturnType; + authConfig?: ReturnType; +}) { + const fallbackDependencies = createToolDependencies({ + ...(options.repoConfig ? { repoConfig: options.repoConfig } : {}), + ...(options.authConfig ? { authConfig: options.authConfig } : {}), + }); + + return executeOrfeTool( + options.input, + { + agent: 'Greg', + cwd: '/tmp/repo', + ...options.context, + }, + options.dependencies ?? fallbackDependencies, + ); +} + +export { createRepoConfig, createRepoConfigWithDefaultProject, createAuthConfig }; diff --git a/test/support/http-test.ts b/test/support/http-test.ts new file mode 100644 index 0000000..d0637cb --- /dev/null +++ b/test/support/http-test.ts @@ -0,0 +1,12 @@ +import nock from 'nock'; + +export async function withNock(testBody: () => Promise): Promise { + nock.disableNetConnect(); + + try { + await testBody(); + } finally { + nock.cleanAll(); + nock.enableNetConnect(); + } +} diff --git a/test/support/runtime-fixtures.ts b/test/support/runtime-fixtures.ts new file mode 100644 index 0000000..3d817cd --- /dev/null +++ b/test/support/runtime-fixtures.ts @@ -0,0 +1,73 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { GitHubClientFactory } from '../../src/github.js'; + +const supportDirectory = path.dirname(fileURLToPath(import.meta.url)); + +export const workspaceRoot = path.resolve(supportDirectory, '../..'); +export const repoConfigPath = path.join(workspaceRoot, '.orfe', 'config.json'); + +export function createRepoConfig(options: { includeDefaultProject?: boolean } = {}) { + const config = { + configPath: repoConfigPath, + version: 1 as const, + repository: { + owner: 'throw-if-null', + name: 'orfe', + defaultBranch: 'main', + }, + callerToBot: { + Greg: 'greg', + }, + }; + + if (!options.includeDefaultProject) { + return config; + } + + return { + ...config, + projects: { + default: { + owner: 'throw-if-null', + projectNumber: 1, + statusFieldName: 'Status', + }, + }, + }; +} + +export function createRepoConfigWithDefaultProject() { + return createRepoConfig({ includeDefaultProject: true }); +} + +export function createAuthConfig() { + return { + configPath: '/tmp/auth.json', + version: 1 as const, + bots: { + greg: { + provider: 'github-app' as const, + appId: 123458, + appSlug: 'GR3G-BOT', + privateKeyPath: '/tmp/greg.pem', + }, + }, + }; +} + +export function createGitHubClientFactory() { + return new GitHubClientFactory({ + readFileImpl: async () => 'private-key', + jwtFactory: () => 'jwt-token', + }); +} + +export function renderIssueBodyContractMarker(): string { + return ''; +} + +export function renderPrBodyContractMarker(): string { + return ''; +} diff --git a/test/wrapper.test.ts b/test/wrapper.test.ts deleted file mode 100644 index 1b29f34..0000000 --- a/test/wrapper.test.ts +++ /dev/null @@ -1,1968 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import nock from 'nock'; -import { test } from 'vitest'; -import { fileURLToPath } from 'node:url'; - -import { GitHubClientFactory } from '../src/github.js'; -import { COMMANDS } from '../src/commands/index.js'; -import { createHelpCommandSuccessData, createHelpRootSuccessData } from '../src/commands/help/definition.js'; -import type { OrfeCoreRequest, SuccessResponse } from '../src/types.js'; -import { executeOrfeTool, resolveCallerNameFromContext } from '../src/wrapper.js'; - -const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const repoConfigPath = path.join(workspaceRoot, '.orfe', 'config.json'); - -function createRepoConfig() { - return { - configPath: repoConfigPath, - version: 1 as const, - repository: { owner: 'throw-if-null', name: 'orfe', defaultBranch: 'main' }, - callerToBot: { Greg: 'greg' }, - }; -} - -function createRepoConfigWithDefaultProject() { - return { - ...createRepoConfig(), - projects: { - default: { - owner: 'throw-if-null', - projectNumber: 1, - statusFieldName: 'Status', - }, - }, - }; -} - -function createAuthConfig() { - return { - configPath: '/tmp/auth.json', - version: 1 as const, - bots: { - greg: { - provider: 'github-app' as const, - appId: 123, - appSlug: 'GR3G-BOT', - privateKeyPath: '/tmp/greg.pem', - }, - }, - }; -} - -function createGitHubClientFactory() { - return new GitHubClientFactory({ - readFileImpl: async () => 'private-key', - jwtFactory: () => 'jwt-token', - }); -} - -function mockAuthTokenMintRequest() { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }); -} - -function mockIssueGetRequest(issueNumber: number) { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) - .reply(200, { - number: issueNumber, - title: 'Build `orfe` foundation and runtime scaffolding', - body: 'Issue body', - state: 'open', - state_reason: null, - labels: [{ name: 'needs-input' }], - assignees: [{ login: 'greg' }], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - }); -} - -function mockIssueUpdateRequest(issueNumber: number, requestBody: Record) { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/issues/${issueNumber}`) - .reply(200, { - number: issueNumber, - title: 'Updated title', - body: 'Updated body', - state: 'open', - state_reason: null, - labels: [{ name: 'bug' }], - assignees: [{ login: 'greg' }], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - }) - .patch(`/repos/throw-if-null/orfe/issues/${issueNumber}`, requestBody) - .reply(200, { - number: issueNumber, - title: 'Updated title', - body: 'Updated body', - state: 'open', - state_reason: null, - labels: [{ name: 'bug' }], - assignees: [{ login: 'greg' }], - html_url: `https://github.com/throw-if-null/orfe/issues/${issueNumber}`, - }); -} - -function mockIssueCreateRequest(requestBody: Record) { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .post('/repos/throw-if-null/orfe/issues', requestBody) - .reply(201, { - number: 21, - node_id: 'I_kwDOOrfeIssue21', - title: requestBody.title, - body: requestBody.body ?? '', - state: 'open', - state_reason: null, - labels: ((requestBody.labels as string[] | undefined) ?? []).map((name) => ({ name })), - assignees: ((requestBody.assignees as string[] | undefined) ?? []).map((login) => ({ login })), - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - }); -} - -function renderIssueBodyContractMarker() { - return ''; -} - -function renderPrBodyContractMarker() { - return ''; -} - -function mockPullRequestGetRequest(prNumber: number) { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply(200, { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }); -} - -function mockPullRequestGetOrCreateRequest(options: { - head: string; - base?: string; - existingPullRequests?: Record[]; - createRequestBody?: Record; - createResponseBody?: Record; -}) { - const head = options.head; - const base = options.base ?? 'main'; - const scope = nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get('/repos/throw-if-null/orfe/pulls') - .query({ state: 'open', head: `throw-if-null:${head}`, base, per_page: 100 }) - .reply(200, options.existingPullRequests ?? []); - - if (options.createRequestBody || options.createResponseBody) { - scope - .post('/repos/throw-if-null/orfe/pulls', options.createRequestBody ?? { - head, - base, - title: 'Design the `orfe` custom tool and CLI contract', - draft: false, - }) - .reply( - 201, - options.createResponseBody ?? { - number: 9, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: head }, - base: { ref: base }, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - ); - } - - return scope; -} - -function mockPullRequestCommentRequest(prNumber: number, body: string) { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply(200, { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }) - .post(`/repos/throw-if-null/orfe/issues/${prNumber}/comments`, { body }) - .reply(201, { - id: 123456, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}#issuecomment-123456`, - }); -} - -function mockPullRequestReplyRequest(prNumber: number, commentId: number, body: string) { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply(200, { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }) - .post(`/repos/throw-if-null/orfe/pulls/${prNumber}/comments/${commentId}/replies`, { body }) - .reply(201, { - id: 123999, - in_reply_to_id: commentId, - }); -} - -function mockPullRequestSubmitReviewRequest( - prNumber: number, - body: string, - event: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT', -) { - return nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .get(`/repos/throw-if-null/orfe/pulls/${prNumber}`) - .reply(200, { - number: prNumber, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: `https://github.com/throw-if-null/orfe/pull/${prNumber}`, - }) - .post(`/repos/throw-if-null/orfe/pulls/${prNumber}/reviews`, { body, event }) - .reply(200, { - id: 555, - }); -} - -function createProjectStatusFieldNode(options: { id: string; name: string; options?: Array<{ id: string; name: string }> }) { - return { - __typename: 'ProjectV2SingleSelectField', - id: options.id, - name: options.name, - options: options.options ?? [], - }; -} - -function createProjectStatusValueNode(options: { fieldId: string; fieldName: string; optionId: string; name: string }) { - return { - __typename: 'ProjectV2ItemFieldSingleSelectValue', - optionId: options.optionId, - name: options.name, - field: { - __typename: 'ProjectV2SingleSelectField', - id: options.fieldId, - name: options.fieldName, - }, - }; -} - -function createProjectItemsConnection(nodes: unknown[]) { - return { - nodes, - pageInfo: { - hasNextPage: false, - endCursor: null, - }, - }; -} - -function createProjectFieldsConnection(nodes: unknown[]) { - return { - nodes, - pageInfo: { - hasNextPage: false, - endCursor: null, - }, - }; -} - -function createProjectItemNode(options: { - id: string; - projectId?: string; - projectOwner: string; - projectNumber: number; - statusValue?: unknown; -}) { - return { - id: options.id, - project: { - id: options.projectId ?? 'PVT_project_1', - number: options.projectNumber, - owner: { - login: options.projectOwner, - }, - }, - fieldValueByName: options.statusValue ?? null, - }; -} - -function matchesProjectByOwnerAndNumber(body: unknown, options: { projectOwner: string; projectNumber: number }): boolean { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('query ProjectByOwnerAndNumber') && - 'variables' in body && - typeof (body as { variables?: unknown }).variables === 'object' && - (body as { variables: { login?: unknown; number?: unknown } }).variables.login === options.projectOwner && - (body as { variables: { login?: unknown; number?: unknown } }).variables.number === options.projectNumber - ); -} - -function createProjectLookupResponse(options: { projectId: string; ownerType?: 'organization' | 'user' }) { - return { - data: { - repositoryOwner: { - __typename: options.ownerType === 'user' ? 'User' : 'Organization', - projectV2: { - id: options.projectId, - }, - }, - }, - }; -} - -function matchesProjectAddItem(body: unknown, options: { projectId: string; contentId: string }): boolean { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('mutation AddProjectItem') && - 'variables' in body && - typeof (body as { variables?: unknown }).variables === 'object' && - (body as { variables: { projectId?: unknown; contentId?: unknown } }).variables.projectId === options.projectId && - (body as { variables: { projectId?: unknown; contentId?: unknown } }).variables.contentId === options.contentId - ); -} - -test('resolveCallerNameFromContext accepts a string agent name', () => { - assert.equal(resolveCallerNameFromContext({ agent: 'Greg' }), 'Greg'); -}); - -test('resolveCallerNameFromContext accepts context.agent.name', () => { - assert.equal(resolveCallerNameFromContext({ agent: { name: 'Jelena' } }), 'Jelena'); -}); - -test('executeOrfeTool forwards caller identity and common path overrides as plain core input', async () => { - let capturedRequest: OrfeCoreRequest | undefined; - let receivedAgentInCore = false; - - const result = await executeOrfeTool( - { - command: 'issue get', - issue_number: 14, - config: '/tmp/.orfe/config.json', - auth_config: '/tmp/auth.json', - }, - { - agent: { name: 'Greg', role: 'implementation-owner' }, - cwd: '/tmp/repo', - }, - { - runOrfeCoreImpl: async (request) => { - capturedRequest = request; - receivedAgentInCore = 'agent' in (request as unknown as Record); - - return { - ok: true, - command: 'issue get', - repo: 'throw-if-null/orfe', - data: { issue_number: 14 }, - } satisfies SuccessResponse>; - }, - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue get', - repo: 'throw-if-null/orfe', - data: { issue_number: 14 }, - }); - assert.deepEqual(capturedRequest, { - callerName: 'Greg', - command: 'issue get', - input: { issue_number: 14 }, - entrypoint: 'opencode-plugin', - configPath: '/tmp/.orfe/config.json', - authConfigPath: '/tmp/auth.json', - cwd: '/tmp/repo', - logger: capturedRequest?.logger, - }); - assert.equal(typeof capturedRequest?.logger?.error, 'function'); - assert.equal(capturedRequest?.logger?.level, 'error'); - assert.equal(receivedAgentInCore, false); -}); - -test('executeOrfeTool returns runtime info through the shared success envelope without caller context', async () => { - const result = await executeOrfeTool( - { - command: 'runtime info', - }, - {}, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - if (result.ok) { - assert.equal(result.command, 'runtime info'); - assert.equal(result.repo, undefined); - assert.match(String((result.data as { orfe_version: string }).orfe_version), /^\d+\.\d+\.\d+/); - assert.deepEqual(result.data, { - orfe_version: (result.data as { orfe_version: string }).orfe_version, - entrypoint: 'opencode-plugin', - }); - } -}); - -test('executeOrfeTool returns root help through the shared success envelope without caller context', async () => { - const result = await executeOrfeTool( - { - command: 'help', - }, - {}, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - if (result.ok) { - assert.equal(result.command, 'help'); - assert.equal(result.repo, undefined); - assert.deepEqual(result.data, createHelpRootSuccessData(COMMANDS)); - } -}); - -test('executeOrfeTool returns targeted command help through the shared success envelope without caller context', async () => { - const result = await executeOrfeTool( - { - command: 'help', - command_name: 'issue get', - }, - {}, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - if (result.ok) { - assert.equal(result.command, 'help'); - assert.equal(result.repo, undefined); - assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, 'issue get')); - } -}); - -test('executeOrfeTool returns representative targeted help across issue, pr, and project commands', async () => { - for (const commandName of ['issue get', 'pr get-or-create', 'project set-status'] as const) { - const result = await executeOrfeTool( - { - command: 'help', - command_name: commandName, - }, - {}, - { - loadRepoConfigImpl: async () => { - throw new Error('loadRepoConfigImpl should not run'); - }, - loadAuthConfigImpl: async () => { - throw new Error('loadAuthConfigImpl should not run'); - }, - }, - ); - - assert.equal(result.ok, true); - if (result.ok) { - assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, commandName)); - } - } -}); - -test('executeOrfeTool returns the shared success envelope for issue get', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueGetRequest(14); - - const result = await executeOrfeTool( - { - command: 'issue get', - issue_number: 14, - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue get', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - title: 'Build `orfe` foundation and runtime scaffolding', - body: 'Issue body', - state: 'open', - state_reason: null, - labels: ['needs-input'], - assignees: ['greg'], - html_url: 'https://github.com/throw-if-null/orfe/issues/14', - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for issue update', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueUpdateRequest(14, { - title: 'Updated title', - labels: ['bug'], - }); - - const result = await executeOrfeTool( - { - command: 'issue update', - issue_number: 14, - title: 'Updated title', - labels: ['bug'], - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue update', - repo: 'throw-if-null/orfe', - data: { - issue_number: 14, - title: 'Updated title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/14', - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for issue create', async () => { - nock.disableNetConnect(); - - try { - const api = mockIssueCreateRequest({ - title: 'New issue title', - body: 'Body text', - labels: ['needs-input'], - assignees: ['greg'], - }); - - const result = await executeOrfeTool( - { - command: 'issue create', - title: 'New issue title', - body: 'Body text', - labels: ['needs-input'], - assignees: ['greg'], - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns project assignment details for issue create when explicitly requested', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - title: 'New issue title', - }); - const api = nock('https://api.github.com') - .post('/graphql', (body: unknown) => matchesProjectByOwnerAndNumber(body, { projectOwner: 'throw-if-null', projectNumber: 1 })) - .reply(200, createProjectLookupResponse({ projectId: 'PVT_project_1', ownerType: 'organization' })) - .post('/graphql', (body: unknown) => matchesProjectAddItem(body, { projectId: 'PVT_project_1', contentId: 'I_kwDOOrfeIssue21' })) - .reply(200, { - data: { - addProjectV2ItemById: { - item: { - id: 'PVTI_lAHOABCD1234', - }, - }, - }, - }); - - const result = await executeOrfeTool( - { - command: 'issue create', - title: 'New issue title', - add_to_project: true, - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfigWithDefaultProject(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - project_assignment: { - project_owner: 'throw-if-null', - project_number: 1, - project_item_id: 'PVTI_lAHOABCD1234', - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns project assignment details for issue create with a user-owned project', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - title: 'New issue title', - }); - const api = nock('https://api.github.com') - .post('/graphql', (body: unknown) => matchesProjectByOwnerAndNumber(body, { projectOwner: 'octocat', projectNumber: 7 })) - .reply(200, createProjectLookupResponse({ projectId: 'PVT_project_user_7', ownerType: 'user' })) - .post('/graphql', (body: unknown) => matchesProjectAddItem(body, { projectId: 'PVT_project_user_7', contentId: 'I_kwDOOrfeIssue21' })) - .reply(200, { - data: { - addProjectV2ItemById: { - item: { - id: 'PVTI_lAHOABCDUSER', - }, - }, - }, - }); - - const result = await executeOrfeTool( - { - command: 'issue create', - title: 'New issue title', - add_to_project: true, - project_owner: 'octocat', - project_number: 7, - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfigWithDefaultProject(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue create', - repo: 'throw-if-null/orfe', - data: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - project_assignment: { - project_owner: 'octocat', - project_number: 7, - project_item_id: 'PVTI_lAHOABCDUSER', - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns partial-failure details for issue create project assignment errors', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - title: 'New issue title', - }); - const api = nock('https://api.github.com') - .post('/graphql', (body: unknown) => matchesProjectByOwnerAndNumber(body, { projectOwner: 'throw-if-null', projectNumber: 1 })) - .reply(200, createProjectLookupResponse({ projectId: 'PVT_project_1', ownerType: 'organization' })) - .post('/graphql', (body: unknown) => matchesProjectAddItem(body, { projectId: 'PVT_project_1', contentId: 'I_kwDOOrfeIssue21' })) - .reply(403, { message: 'Resource not accessible by integration' }); - - const result = await executeOrfeTool( - { - command: 'issue create', - title: 'New issue title', - add_to_project: true, - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfigWithDefaultProject(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: false, - command: 'issue create', - error: { - code: 'auth_failed', - message: - 'Issue #21 was created, but adding it to GitHub Project throw-if-null/1 failed: GitHub App authentication failed while adding issue #21 to a GitHub Project.', - retryable: false, - details: { - stage: 'project_add', - created_issue: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - project_owner: 'throw-if-null', - project_number: 1, - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns project_add partial-failure details when status was requested but project add failed', async () => { - nock.disableNetConnect(); - - try { - const issueApi = mockIssueCreateRequest({ - title: 'New issue title', - }); - const api = nock('https://api.github.com') - .post('/graphql', (body: unknown) => matchesProjectByOwnerAndNumber(body, { projectOwner: 'throw-if-null', projectNumber: 1 })) - .reply(200, createProjectLookupResponse({ projectId: 'PVT_project_1', ownerType: 'organization' })) - .post('/graphql', (body: unknown) => matchesProjectAddItem(body, { projectId: 'PVT_project_1', contentId: 'I_kwDOOrfeIssue21' })) - .reply(403, { message: 'Resource not accessible by integration' }); - - const result = await executeOrfeTool( - { - command: 'issue create', - title: 'New issue title', - status: 'Todo', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfigWithDefaultProject(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: false, - command: 'issue create', - error: { - code: 'auth_failed', - message: - 'Issue #21 was created, but adding it to GitHub Project throw-if-null/1 failed: GitHub App authentication failed while adding issue #21 to a GitHub Project.', - retryable: false, - details: { - stage: 'project_add', - created_issue: { - issue_number: 21, - title: 'New issue title', - state: 'open', - html_url: 'https://github.com/throw-if-null/orfe/issues/21', - created: true, - }, - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - requested_status: 'Todo', - }, - }, - }); - assert.equal(issueApi.isDone(), true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool validates issue bodies through body contracts before create', async () => { - nock.disableNetConnect(); - - try { - const issueBody = [ - '## Problem / context', - '', - 'Need deterministic validation for issue bodies.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against declarative contracts.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: add new durable docs', - '', - '## ADR needed?', - '', - '- ADR needed: yes', - ].join('\n'); - - const api = mockIssueCreateRequest({ - title: 'New issue title', - body: `${issueBody}\n\n${renderIssueBodyContractMarker()}`, - }); - - const result = await executeOrfeTool( - { - command: 'issue create', - title: 'New issue title', - body: issueBody, - body_contract: 'formal-work-item@1.0.0', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for pr get', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetRequest(9); - - const result = await executeOrfeTool( - { - command: 'pr get', - pr_number: 9, - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr get', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: 'issues/orfe-13', - base: 'main', - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for pr get-or-create', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-13', - existingPullRequests: [ - { - number: 9, - title: 'Design the `orfe` custom tool and CLI contract', - body: 'PR body', - state: 'open', - draft: false, - head: { ref: 'issues/orfe-13' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - }, - ], - }); - - const result = await executeOrfeTool( - { - command: 'pr get-or-create', - head: 'issues/orfe-13', - title: 'Design the `orfe` custom tool and CLI contract', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr get-or-create', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - html_url: 'https://github.com/throw-if-null/orfe/pull/9', - head: 'issues/orfe-13', - base: 'main', - draft: false, - created: false, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool validates PR bodies through body contracts before create', async () => { - nock.disableNetConnect(); - - try { - const prBody = [ - 'Ref: #59', - '', - '## Summary', - '', - '- add body-contract support', - '', - '## Verification', - '', - '- `npm test` ✅', - '- `npm run lint` ✅', - '- `npm run typecheck` ✅', - '- `npm run build` ✅', - '', - '## Docs / ADR / debt', - '', - '- docs updated: yes', - '- ADR updated: yes', - '- debt updated: yes', - '- details: updated docs and added ADR', - '', - '## Risks / follow-ups', - '', - '- richer generation is follow-up work', - ].join('\n'); - - const api = mockPullRequestGetOrCreateRequest({ - head: 'issues/orfe-59', - existingPullRequests: [], - createRequestBody: { - head: 'issues/orfe-59', - base: 'main', - title: 'Introduce versioned body-contract support', - body: `${prBody}\n\n${renderPrBodyContractMarker()}`, - draft: false, - }, - createResponseBody: { - number: 59, - title: 'Introduce versioned body-contract support', - body: `${prBody}\n\n${renderPrBodyContractMarker()}`, - state: 'open', - draft: false, - head: { ref: 'issues/orfe-59' }, - base: { ref: 'main' }, - html_url: 'https://github.com/throw-if-null/orfe/pull/59', - }, - }); - - const result = await executeOrfeTool( - { - command: 'pr get-or-create', - head: 'issues/orfe-59', - title: 'Introduce versioned body-contract support', - body: prBody, - body_contract: 'implementation-ready@1.0.0', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.equal(result.ok, true); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns structured PR validation output', async () => { - nock.disableNetConnect(); - - try { - const result = await executeOrfeTool( - { - command: 'pr validate', - body: 'Ref: #58\n\nCloses: #58', - body_contract: 'implementation-ready@1.0.0', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr validate', - repo: 'throw-if-null/orfe', - data: { - valid: false, - contract: { - artifact_type: 'pr', - contract_name: 'implementation-ready', - contract_version: '1.0.0', - }, - contract_source: 'explicit', - errors: [ - { - kind: 'matched_forbidden_pattern', - scope: 'body', - pattern: '(?:^|\\n)(?:Closes|Close|Closed|Fixes|Fix|Fixed|Resolves|Resolve|Resolved)\\s*:?\\s*#\\d+', - message: - 'Body contract validation failed: body matched forbidden pattern (?:^|\\n)(?:Closes|Close|Closed|Fixes|Fix|Fixed|Resolves|Resolve|Resolved)\\s*:?\\s*#\\d+.', - }, - { - kind: 'missing_required_section', - scope: 'section', - section_heading: 'Summary', - message: 'Body contract validation failed: missing required section "Summary".', - }, - { - kind: 'missing_required_section', - scope: 'section', - section_heading: 'Verification', - message: 'Body contract validation failed: missing required section "Verification".', - }, - { - kind: 'missing_required_section', - scope: 'section', - section_heading: 'Docs / ADR / debt', - message: 'Body contract validation failed: missing required section "Docs / ADR / debt".', - }, - { - kind: 'missing_required_section', - scope: 'section', - section_heading: 'Risks / follow-ups', - message: 'Body contract validation failed: missing required section "Risks / follow-ups".', - }, - ], - }, - }); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns structured issue validation output', async () => { - nock.disableNetConnect(); - - try { - const result = await executeOrfeTool( - { - command: 'issue validate', - body: [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: update existing docs', - '- Details: update docs/orfe/spec.md', - '', - '## ADR needed?', - '', - '- ADR needed: no', - '- Details: covered by ADR 0009', - '', - '## Dependencies / sequencing notes', - '', - '- depends on #59', - '', - '## Risks / open questions / non-goals', - '', - '- keep repo-specific structure out of runtime logic', - ].join('\n'), - body_contract: 'formal-work-item@1.0.0', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'issue validate', - repo: 'throw-if-null/orfe', - data: { - valid: true, - contract: { - artifact_type: 'issue', - contract_name: 'formal-work-item', - contract_version: '1.0.0', - }, - contract_source: 'explicit', - normalized_body: [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against a versioned contract.', - '', - '## Scope', - '', - '### In scope', - '- declarative contracts', - '', - '### Out of scope', - '- executable plugins', - '', - '## Acceptance criteria', - '', - '- [ ] contracts load from .orfe/contracts', - '', - '## Docs impact', - '', - '- Docs impact: update existing docs', - '- Details: update docs/orfe/spec.md', - '', - '## ADR needed?', - '', - '- ADR needed: no', - '- Details: covered by ADR 0009', - '', - '## Dependencies / sequencing notes', - '', - '- depends on #59', - '', - '## Risks / open questions / non-goals', - '', - '- keep repo-specific structure out of runtime logic', - '', - renderIssueBodyContractMarker(), - ].join('\n'), - errors: [], - }, - }); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns structured config failures for issue validate when config path is invalid', async () => { - const missingConfigPath = path.join(workspaceRoot, 'missing-issue-validate-config.json'); - - const result = await executeOrfeTool( - { - command: 'issue validate', - body: [ - '## Problem / context', - '', - 'Need deterministic issue-body validation.', - '', - '## Desired outcome', - '', - 'Issue bodies validate against a versioned contract.', - ].join('\n'), - body_contract: 'formal-work-item@1.0.0', - config: missingConfigPath, - }, - { - agent: 'Greg', - cwd: workspaceRoot, - }, - ); - - assert.deepEqual(result, { - ok: false, - command: 'issue validate', - error: { - code: 'config_not_found', - message: `repo-local config not found at ${missingConfigPath}.`, - retryable: false, - }, - }); -}); - -test('executeOrfeTool returns the shared success envelope for pr comment', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestCommentRequest(9, 'Hello from orfe'); - - const result = await executeOrfeTool( - { - command: 'pr comment', - pr_number: 9, - body: 'Hello from orfe', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr comment', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - comment_id: 123456, - html_url: 'https://github.com/throw-if-null/orfe/pull/9#issuecomment-123456', - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for pr submit-review', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestSubmitReviewRequest(9, 'Looks good', 'APPROVE'); - - const result = await executeOrfeTool( - { - command: 'pr submit-review', - pr_number: 9, - event: 'approve', - body: 'Looks good', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr submit-review', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - review_id: 555, - event: 'approve', - submitted: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for pr reply', async () => { - nock.disableNetConnect(); - - try { - const api = mockPullRequestReplyRequest(9, 123456, 'ack'); - - const result = await executeOrfeTool( - { - command: 'pr reply', - pr_number: 9, - comment_id: 123456, - body: 'ack', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'pr reply', - repo: 'throw-if-null/orfe', - data: { - pr_number: 9, - comment_id: 123999, - in_reply_to_comment_id: 123456, - created: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for project get-status', async () => { - nock.disableNetConnect(); - - try { - const api = nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .post('/graphql', (body: unknown) => { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('query ProjectStatusForIssue') - ); - }) - .reply(200, { - data: { - repository: { - issue: { - projectItems: { - nodes: [ - { - id: 'PVTI_lAHOABCD1234', - project: { - id: 'PVT_project_1', - number: 1, - owner: { - login: 'throw-if-null', - }, - }, - fieldValueByName: { - __typename: 'ProjectV2ItemFieldSingleSelectValue', - optionId: 'f75ad846', - name: 'In Progress', - field: { - __typename: 'ProjectV2SingleSelectField', - id: 'PVTSSF_lAHOABCD1234', - name: 'Status', - }, - }, - }, - ], - pageInfo: { - hasNextPage: false, - endCursor: null, - }, - }, - }, - }, - }, - }) - .post('/graphql', (body: unknown) => { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('query ProjectStatusFields') - ); - }) - .reply(200, { - data: { - node: { - fields: { - nodes: [ - { - __typename: 'ProjectV2SingleSelectField', - id: 'PVTSSF_lAHOABCD1234', - name: 'Status', - }, - ], - pageInfo: { - hasNextPage: false, - endCursor: null, - }, - }, - }, - }, - }); - - const result = await executeOrfeTool( - { - command: 'project get-status', - item_type: 'issue', - item_number: 13, - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfigWithDefaultProject(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project get-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad846', - status: 'In Progress', - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool returns the shared success envelope for project set-status', async () => { - nock.disableNetConnect(); - - try { - const api = nock('https://api.github.com') - .get('/repos/throw-if-null/orfe/installation') - .reply(200, { id: 42 }) - .post('/app/installations/42/access_tokens') - .reply(201, { token: 'ghs_123', expires_at: '2026-04-06T12:00:00Z' }) - .post('/graphql', (body: unknown) => { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('query ProjectStatusForIssue') - ); - }) - .reply(200, { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad845', - name: 'Todo', - }), - }), - ]), - }, - }, - }, - }) - .post('/graphql', (body: unknown) => { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('query ProjectStatusFields') - ); - }) - .reply(200, { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ - id: 'PVTSSF_lAHOABCD1234', - name: 'Status', - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - ], - }), - ]), - }, - }, - }) - .post('/graphql', (body: unknown) => { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('mutation UpdateProjectStatus') - ); - }) - .reply(200, { - data: { - updateProjectV2ItemFieldValue: { - clientMutationId: null, - }, - }, - }) - .post('/graphql', (body: unknown) => { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('query ProjectStatusForIssue') - ); - }) - .reply(200, { - data: { - repository: { - issue: { - projectItems: createProjectItemsConnection([ - createProjectItemNode({ - id: 'PVTI_lAHOABCD1234', - projectId: 'PVT_project_1', - projectOwner: 'throw-if-null', - projectNumber: 1, - statusValue: createProjectStatusValueNode({ - fieldId: 'PVTSSF_lAHOABCD1234', - fieldName: 'Status', - optionId: 'f75ad846', - name: 'In Progress', - }), - }), - ]), - }, - }, - }, - }) - .post('/graphql', (body: unknown) => { - return ( - typeof body === 'object' && - body !== null && - 'query' in body && - typeof (body as { query?: unknown }).query === 'string' && - (body as { query: string }).query.includes('query ProjectStatusFields') - ); - }) - .reply(200, { - data: { - node: { - fields: createProjectFieldsConnection([ - createProjectStatusFieldNode({ - id: 'PVTSSF_lAHOABCD1234', - name: 'Status', - options: [ - { id: 'f75ad845', name: 'Todo' }, - { id: 'f75ad846', name: 'In Progress' }, - ], - }), - ]), - }, - }, - }); - - const result = await executeOrfeTool( - { - command: 'project set-status', - item_type: 'issue', - item_number: 13, - status: 'In Progress', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfigWithDefaultProject(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'project set-status', - repo: 'throw-if-null/orfe', - data: { - project_owner: 'throw-if-null', - project_number: 1, - status_field_name: 'Status', - status_field_id: 'PVTSSF_lAHOABCD1234', - item_type: 'issue', - item_number: 13, - project_item_id: 'PVTI_lAHOABCD1234', - status_option_id: 'f75ad846', - status: 'In Progress', - previous_status_option_id: 'f75ad845', - previous_status: 'Todo', - changed: true, - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool rejects missing caller context clearly', async () => { - const result = await executeOrfeTool( - { - command: 'issue get', - issue_number: 14, - }, - {}, - ); - - assert.deepEqual(result, { - ok: false, - command: 'issue get', - error: { - code: 'caller_context_missing', - message: 'OpenCode caller context is missing.', - retryable: false, - }, - }); -}); - -test('executeOrfeTool still rejects missing caller context for caller-mapped commands', async () => { - const result = await executeOrfeTool( - { - command: 'issue get', - issue_number: 14, - }, - {}, - ); - - assert.deepEqual(result, { - ok: false, - command: 'issue get', - error: { - code: 'caller_context_missing', - message: 'OpenCode caller context is missing.', - retryable: false, - }, - }); -}); - -test('executeOrfeTool resolves auth token from context.agent and returns shared success envelope', async () => { - nock.disableNetConnect(); - - try { - const api = mockAuthTokenMintRequest(); - - const result = await executeOrfeTool( - { - command: 'auth token', - repo: 'throw-if-null/orfe', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - { - loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - }, - ); - - assert.deepEqual(result, { - ok: true, - command: 'auth token', - repo: 'throw-if-null/orfe', - data: { - bot: 'greg', - app_slug: 'GR3G-BOT', - repo: 'throw-if-null/orfe', - token: 'ghs_123', - expires_at: '2026-04-06T12:00:00Z', - auth_mode: 'github-app', - }, - }); - assert.equal(api.isDone(), true); - } finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -}); - -test('executeOrfeTool rejects bot override input for auth token', async () => { - const result = await executeOrfeTool( - { - command: 'auth token', - bot: 'greg', - repo: 'throw-if-null/orfe', - }, - { - agent: 'Greg', - cwd: '/tmp/repo', - }, - ); - - assert.deepEqual(result, { - ok: false, - command: 'auth token', - error: { - code: 'invalid_usage', - message: 'Command "auth token" does not accept input field "bot".', - retryable: false, - }, - }); -}); - -test('executeOrfeTool rejects caller_name from tool input', async () => { - const result = await executeOrfeTool( - { - command: 'issue get', - caller_name: 'Greg', - issue_number: 14, - }, - { - agent: 'Greg', - }, - ); - - assert.deepEqual(result, { - ok: false, - command: 'issue get', - error: { - code: 'invalid_usage', - message: 'Tool input does not accept caller_name; caller identity comes from context.agent.', - retryable: false, - }, - }); -}); diff --git a/test/wrapper/opencode-context.test.ts b/test/wrapper/opencode-context.test.ts new file mode 100644 index 0000000..0fb29d5 --- /dev/null +++ b/test/wrapper/opencode-context.test.ts @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { executeOrfeTool, resolveCallerNameFromContext } from '../../src/wrapper.js'; + +test('resolveCallerNameFromContext accepts a string agent name', () => { + assert.equal(resolveCallerNameFromContext({ agent: 'Greg' }), 'Greg'); +}); + +test('resolveCallerNameFromContext accepts context.agent.name', () => { + assert.equal(resolveCallerNameFromContext({ agent: { name: 'Jelena' } }), 'Jelena'); +}); + +test('executeOrfeTool rejects missing caller context clearly', async () => { + const result = await executeOrfeTool( + { + command: 'issue get', + issue_number: 14, + }, + {}, + ); + + assert.deepEqual(result, { + ok: false, + command: 'issue get', + error: { + code: 'caller_context_missing', + message: 'OpenCode caller context is missing.', + retryable: false, + }, + }); +}); + +test('executeOrfeTool rejects caller_name from tool input', async () => { + const result = await executeOrfeTool( + { + command: 'issue get', + caller_name: 'Greg', + issue_number: 14, + }, + { + agent: 'Greg', + }, + ); + + assert.deepEqual(result, { + ok: false, + command: 'issue get', + error: { + code: 'invalid_usage', + message: 'Tool input does not accept caller_name; caller identity comes from context.agent.', + retryable: false, + }, + }); +}); diff --git a/test/wrapper/path-overrides.test.ts b/test/wrapper/path-overrides.test.ts new file mode 100644 index 0000000..1e6e49d --- /dev/null +++ b/test/wrapper/path-overrides.test.ts @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { test } from 'vitest'; + +import type { OrfeCoreRequest, SuccessResponse } from '../../src/types.js'; +import { executeOrfeTool } from '../../src/wrapper.js'; +import { workspaceRoot } from '../support/runtime-fixtures.js'; + +test('executeOrfeTool forwards caller identity and common path overrides as plain core input', async () => { + let capturedRequest: OrfeCoreRequest | undefined; + let receivedAgentInCore = false; + + const result = await executeOrfeTool( + { + command: 'issue get', + issue_number: 14, + config: '/tmp/.orfe/config.json', + auth_config: '/tmp/auth.json', + }, + { + agent: { name: 'Greg', role: 'implementation-owner' }, + cwd: '/tmp/repo', + }, + { + runOrfeCoreImpl: async (request) => { + capturedRequest = request; + receivedAgentInCore = 'agent' in (request as unknown as Record); + + return { + ok: true, + command: 'issue get', + repo: 'throw-if-null/orfe', + data: { issue_number: 14 }, + } satisfies SuccessResponse>; + }, + }, + ); + + assert.deepEqual(result, { + ok: true, + command: 'issue get', + repo: 'throw-if-null/orfe', + data: { issue_number: 14 }, + }); + assert.deepEqual(capturedRequest, { + callerName: 'Greg', + command: 'issue get', + input: { issue_number: 14 }, + entrypoint: 'opencode-plugin', + configPath: '/tmp/.orfe/config.json', + authConfigPath: '/tmp/auth.json', + cwd: '/tmp/repo', + logger: capturedRequest?.logger, + }); + assert.equal(typeof capturedRequest?.logger?.error, 'function'); + assert.equal(capturedRequest?.logger?.level, 'error'); + assert.equal(receivedAgentInCore, false); +}); + +test('executeOrfeTool returns structured config failures when forwarded config path is invalid', async () => { + const missingConfigPath = path.join(workspaceRoot, 'missing-issue-validate-config.json'); + + const result = await executeOrfeTool( + { + command: 'issue validate', + body: [ + '## Problem / context', + '', + 'Need deterministic issue-body validation.', + '', + '## Desired outcome', + '', + 'Issue bodies validate against a versioned contract.', + ].join('\n'), + body_contract: 'formal-work-item@1.0.0', + config: missingConfigPath, + }, + { + agent: 'Greg', + cwd: workspaceRoot, + }, + ); + + assert.deepEqual(result, { + ok: false, + command: 'issue validate', + error: { + code: 'config_not_found', + message: `repo-local config not found at ${missingConfigPath}.`, + retryable: false, + }, + }); +}); diff --git a/test/wrapper/runtime-routing.test.ts b/test/wrapper/runtime-routing.test.ts new file mode 100644 index 0000000..5ae84c1 --- /dev/null +++ b/test/wrapper/runtime-routing.test.ts @@ -0,0 +1,108 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { COMMANDS } from '../../src/commands/index.js'; +import { createHelpCommandSuccessData, createHelpRootSuccessData } from '../../src/commands/help/definition.js'; +import { executeOrfeTool } from '../../src/wrapper.js'; + +test('executeOrfeTool returns runtime info through the shared success envelope without caller context', async () => { + const result = await executeOrfeTool( + { + command: 'runtime info', + }, + {}, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.command, 'runtime info'); + assert.equal(result.repo, undefined); + assert.match(String((result.data as { orfe_version: string }).orfe_version), /^\d+\.\d+\.\d+/); + assert.deepEqual(result.data, { + orfe_version: (result.data as { orfe_version: string }).orfe_version, + entrypoint: 'opencode-plugin', + }); + } +}); + +test('executeOrfeTool returns root help through the shared success envelope without caller context', async () => { + const result = await executeOrfeTool( + { + command: 'help', + }, + {}, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.command, 'help'); + assert.equal(result.repo, undefined); + assert.deepEqual(result.data, createHelpRootSuccessData(COMMANDS)); + } +}); + +test('executeOrfeTool returns targeted command help through the shared success envelope without caller context', async () => { + const result = await executeOrfeTool( + { + command: 'help', + command_name: 'issue get', + }, + {}, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.command, 'help'); + assert.equal(result.repo, undefined); + assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, 'issue get')); + } +}); + +test('executeOrfeTool returns representative targeted help across issue, pr, and project commands', async () => { + for (const commandName of ['issue get', 'pr get-or-create', 'project set-status'] as const) { + const result = await executeOrfeTool( + { + command: 'help', + command_name: commandName, + }, + {}, + { + loadRepoConfigImpl: async () => { + throw new Error('loadRepoConfigImpl should not run'); + }, + loadAuthConfigImpl: async () => { + throw new Error('loadAuthConfigImpl should not run'); + }, + }, + ); + + assert.equal(result.ok, true); + if (result.ok) { + assert.deepEqual(result.data, createHelpCommandSuccessData(COMMANDS, commandName)); + } + } +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index 8441ec3..82b508b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,5 +5,5 @@ "outDir": "./dist" }, "include": ["src/**/*.ts"], - "exclude": ["test/**/*"] + "exclude": ["test/**/*", "src/**/*.test.ts"] } diff --git a/tsconfig.json b/tsconfig.json index c953edf..f331f3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "resolveJsonModule": true, "declaration": true, "sourceMap": true, - "types": ["node"] + "types": ["node", "vitest/globals"] }, "include": ["src/**/*.ts", "src/**/*.d.ts", "test/**/*.test.ts"] }