From 87b4f2ec6933c0b40e8e7c7ebbc03e94c876be88 Mon Sep 17 00:00:00 2001 From: Mirza Merdovic Date: Sat, 9 May 2026 12:11:13 +0200 Subject: [PATCH 1/5] Refactor test ownership into slice-local and concern-based files (#140) --- src/commands/auth/token/handler.test.ts | 96 + src/commands/issue/comment/handler.test.ts | 127 + src/commands/issue/create/handler.test.ts | 940 +++ src/commands/issue/get/handler.test.ts | 104 + src/commands/issue/set-state/handler.test.ts | 346 + src/commands/issue/update/handler.test.ts | 254 + src/commands/issue/validate/handler.test.ts | 311 + src/commands/pr/comment/handler.test.ts | 143 + src/commands/pr/get-or-create/handler.test.ts | 411 ++ src/commands/pr/get/handler.test.ts | 101 + src/commands/pr/reply/handler.test.ts | 150 + src/commands/pr/submit-review/handler.test.ts | 141 + src/commands/pr/validate/handler.test.ts | 200 + .../project/get-status/handler.test.ts | 473 ++ .../project/set-status/handler.test.ts | 507 ++ test/core.test.ts | 5875 ----------------- test/core/auth-config-loading.test.ts | 241 + test/core/plain-data-boundary.test.ts | 34 + test/core/runtime-routing.test.ts | 108 + test/support/auth-fixtures.js | 16 + test/support/auth-fixtures.js.map | 1 + test/support/auth-fixtures.ts | 23 + test/support/command-runtime.js | 36 + test/support/command-runtime.js.map | 1 + test/support/command-runtime.ts | 85 + test/support/http-test.js | 12 + test/support/http-test.js.map | 1 + test/support/http-test.ts | 12 + test/support/issue-fixtures.js | 224 + test/support/issue-fixtures.js.map | 1 + test/support/issue-fixtures.ts | 328 + test/support/pr-fixtures.js | 136 + test/support/pr-fixtures.js.map | 1 + test/support/pr-fixtures.ts | 210 + test/support/project-fixtures.js | 220 + test/support/project-fixtures.js.map | 1 + test/support/project-fixtures.ts | 319 + test/support/runtime-fixtures.js | 63 + test/support/runtime-fixtures.js.map | 1 + test/support/runtime-fixtures.ts | 73 + test/wrapper.test.ts | 1968 ------ test/wrapper/opencode-context.test.ts | 75 + test/wrapper/path-overrides.test.ts | 128 + test/wrapper/runtime-routing.test.ts | 108 + tsconfig.build.json | 2 +- tsconfig.json | 2 +- 46 files changed, 6764 insertions(+), 7845 deletions(-) create mode 100644 src/commands/auth/token/handler.test.ts create mode 100644 src/commands/issue/comment/handler.test.ts create mode 100644 src/commands/issue/create/handler.test.ts create mode 100644 src/commands/issue/get/handler.test.ts create mode 100644 src/commands/issue/set-state/handler.test.ts create mode 100644 src/commands/issue/update/handler.test.ts create mode 100644 src/commands/issue/validate/handler.test.ts create mode 100644 src/commands/pr/comment/handler.test.ts create mode 100644 src/commands/pr/get-or-create/handler.test.ts create mode 100644 src/commands/pr/get/handler.test.ts create mode 100644 src/commands/pr/reply/handler.test.ts create mode 100644 src/commands/pr/submit-review/handler.test.ts create mode 100644 src/commands/pr/validate/handler.test.ts create mode 100644 src/commands/project/get-status/handler.test.ts create mode 100644 src/commands/project/set-status/handler.test.ts delete mode 100644 test/core.test.ts create mode 100644 test/core/auth-config-loading.test.ts create mode 100644 test/core/plain-data-boundary.test.ts create mode 100644 test/core/runtime-routing.test.ts create mode 100644 test/support/auth-fixtures.js create mode 100644 test/support/auth-fixtures.js.map create mode 100644 test/support/auth-fixtures.ts create mode 100644 test/support/command-runtime.js create mode 100644 test/support/command-runtime.js.map create mode 100644 test/support/command-runtime.ts create mode 100644 test/support/http-test.js create mode 100644 test/support/http-test.js.map create mode 100644 test/support/http-test.ts create mode 100644 test/support/issue-fixtures.js create mode 100644 test/support/issue-fixtures.js.map create mode 100644 test/support/issue-fixtures.ts create mode 100644 test/support/pr-fixtures.js create mode 100644 test/support/pr-fixtures.js.map create mode 100644 test/support/pr-fixtures.ts create mode 100644 test/support/project-fixtures.js create mode 100644 test/support/project-fixtures.js.map create mode 100644 test/support/project-fixtures.ts create mode 100644 test/support/runtime-fixtures.js create mode 100644 test/support/runtime-fixtures.js.map create mode 100644 test/support/runtime-fixtures.ts delete mode 100644 test/wrapper.test.ts create mode 100644 test/wrapper/opencode-context.test.ts create mode 100644 test/wrapper/path-overrides.test.ts create mode 100644 test/wrapper/runtime-routing.test.ts diff --git a/src/commands/auth/token/handler.test.ts b/src/commands/auth/token/handler.test.ts new file mode 100644 index 0000000..0123eeb --- /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/support/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..7f7e9d7 --- /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/support/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..2b54ffd --- /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/support/issue-fixtures.js'; +import { + createProjectFieldsConnection, + createProjectItemNode, + createProjectItemsConnection, + createProjectLookupResponse, + createProjectStatusFieldNode, + createProjectStatusValueNode, + mockProjectAddItemRequest, + mockProjectGetStatusRequest, + mockProjectLookupRequest, + mockProjectStatusFieldsRequest, + mockProjectStatusUpdateRequest, +} from '../../../../test/support/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..bd3f127 --- /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/support/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..4e00db5 --- /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/support/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..49e88f1 --- /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/support/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..1aa1abf --- /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/support/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..1b3b743 --- /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/support/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..3e68094 --- /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/support/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..2b0b949 --- /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/support/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..83c63ba --- /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/support/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..8f3a4f4 --- /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/support/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..636d32f --- /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/support/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..9903ca1 --- /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/support/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/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..11c0c4b --- /dev/null +++ b/test/core/auth-config-loading.test.ts @@ -0,0 +1,241 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { OrfeError } from '../../src/errors.js'; +import { runOrfeCore } from '../../src/core.js'; +import { mockAuthTokenMintRequest } from '../support/auth-fixtures.js'; +import { withNock } from '../support/http-test.js'; +import { createAuthConfig, createRepoConfig } from '../support/runtime-fixtures.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 { createGitHubClientFactory } = await import('../support/runtime-fixtures.js'); + 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); + }); +}); + +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 () => { + await withNock(async () => { + const api = mockAuthTokenMintRequest({ installationStatus: 404 }); + const { createGitHubClientFactory } = await import('../support/runtime-fixtures.js'); + + 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); + }); +}); + +test('runOrfeCore fails clearly for auth token when token minting is rejected', async () => { + await withNock(async () => { + const api = mockAuthTokenMintRequest({ tokenStatus: 403 }); + const { createGitHubClientFactory } = await import('../support/runtime-fixtures.js'); + + 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); + }); +}); + +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 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/plain-data-boundary.test.ts b/test/core/plain-data-boundary.test.ts new file mode 100644 index 0000000..7b3a14a --- /dev/null +++ b/test/core/plain-data-boundary.test.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import { runCoreCommand } from '../support/command-runtime.js'; +import { mockIssueGetRequest } from '../support/issue-fixtures.js'; +import { withNock } from '../support/http-test.js'; + +test('runOrfeCore can be exercised directly with plain callerName data', 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); + }); +}); 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/support/auth-fixtures.js b/test/support/auth-fixtures.js new file mode 100644 index 0000000..b37d3b5 --- /dev/null +++ b/test/support/auth-fixtures.js @@ -0,0 +1,16 @@ +import nock from 'nock'; +export function mockAuthTokenMintRequest(options = {}) { + 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; +} +//# sourceMappingURL=auth-fixtures.js.map \ No newline at end of file diff --git a/test/support/auth-fixtures.js.map b/test/support/auth-fixtures.js.map new file mode 100644 index 0000000..e8e71fa --- /dev/null +++ b/test/support/auth-fixtures.js.map @@ -0,0 +1 @@ +{"version":3,"file":"auth-fixtures.js","sourceRoot":"","sources":["auth-fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,UAAU,wBAAwB,CAAC,UAIrC,EAAE;IACJ,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,KAAK,IAAI,eAAe,CAAC;IACrD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC;IAE1C,MAAM,KAAK,GAAG,IAAI,CAAC,wBAAwB,CAAC,CAAC,GAAG,CAAC,UAAU,KAAK,IAAI,IAAI,eAAe,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,kBAAkB,IAAI,GAAG,EAAE;QAChI,EAAE,EAAE,EAAE;KACP,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,CAAC,kBAAkB,IAAI,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,IAAI,GAAG,EAAE;YAClF,KAAK,EAAE,SAAS;YAChB,UAAU,EAAE,sBAAsB;SACnC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"} \ No newline at end of file diff --git a/test/support/auth-fixtures.ts b/test/support/auth-fixtures.ts new file mode 100644 index 0000000..1dd5785 --- /dev/null +++ b/test/support/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/support/command-runtime.js b/test/support/command-runtime.js new file mode 100644 index 0000000..3481608 --- /dev/null +++ b/test/support/command-runtime.js @@ -0,0 +1,36 @@ +import { runOrfeCore } from '../../src/core.js'; +import { executeOrfeTool } from '../../src/wrapper.js'; +import { createAuthConfig, createGitHubClientFactory, createRepoConfig, createRepoConfigWithDefaultProject, } from './runtime-fixtures.js'; +export function createCoreDependencies(options = {}) { + return { + loadRepoConfigImpl: async () => options.repoConfig ?? createRepoConfig(), + loadAuthConfigImpl: async () => options.authConfig ?? createAuthConfig(), + githubClientFactory: createGitHubClientFactory(), + ...options.overrides, + }; +} +export async function runCoreCommand(options) { + return runOrfeCore({ + callerName: 'Greg', + command: options.command, + input: options.input, + ...options.request, + }, options.dependencies ?? createCoreDependencies({ repoConfig: options.repoConfig, authConfig: options.authConfig })); +} +export function createToolDependencies(options = {}) { + return { + loadRepoConfigImpl: async () => options.repoConfig ?? createRepoConfig(), + loadAuthConfigImpl: async () => options.authConfig ?? createAuthConfig(), + githubClientFactory: createGitHubClientFactory(), + ...options.overrides, + }; +} +export async function runToolCommand(options) { + return executeOrfeTool(options.input, { + agent: 'Greg', + cwd: '/tmp/repo', + ...options.context, + }, options.dependencies ?? createToolDependencies({ repoConfig: options.repoConfig, authConfig: options.authConfig })); +} +export { createRepoConfig, createRepoConfigWithDefaultProject, createAuthConfig }; +//# sourceMappingURL=command-runtime.js.map \ No newline at end of file diff --git a/test/support/command-runtime.js.map b/test/support/command-runtime.js.map new file mode 100644 index 0000000..53c3917 --- /dev/null +++ b/test/support/command-runtime.js.map @@ -0,0 +1 @@ +{"version":3,"file":"command-runtime.js","sourceRoot":"","sources":["command-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA6B,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAuD,MAAM,sBAAsB,CAAC;AAG5G,OAAO,EACL,gBAAgB,EAChB,yBAAyB,EACzB,gBAAgB,EAChB,kCAAkC,GACnC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,UAAU,sBAAsB,CAAC,UAInC,EAAE;IACJ,OAAO;QACL,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,mBAAmB,EAAE,yBAAyB,EAAE;QAChD,GAAG,OAAO,CAAC,SAAS;KACrB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAOpC;IACC,OAAO,WAAW,CAChB;QACE,UAAU,EAAE,MAAM;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,GAAG,OAAO,CAAC,OAAO;KACnB,EACD,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CACnH,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,UAInC,EAAE;IACJ,OAAO;QACL,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,mBAAmB,EAAE,yBAAyB,EAAE;QAChD,GAAG,OAAO,CAAC,SAAS;KACrB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAMpC;IACC,OAAO,eAAe,CACpB,OAAO,CAAC,KAAK,EACb;QACE,KAAK,EAAE,MAAM;QACb,GAAG,EAAE,WAAW;QAChB,GAAG,OAAO,CAAC,OAAO;KACnB,EACD,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CACnH,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,kCAAkC,EAAE,gBAAgB,EAAE,CAAC"} \ No newline at end of file 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.js b/test/support/http-test.js new file mode 100644 index 0000000..69a23fa --- /dev/null +++ b/test/support/http-test.js @@ -0,0 +1,12 @@ +import nock from 'nock'; +export async function withNock(testBody) { + nock.disableNetConnect(); + try { + await testBody(); + } + finally { + nock.cleanAll(); + nock.enableNetConnect(); + } +} +//# sourceMappingURL=http-test.js.map \ No newline at end of file diff --git a/test/support/http-test.js.map b/test/support/http-test.js.map new file mode 100644 index 0000000..554362f --- /dev/null +++ b/test/support/http-test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"http-test.js","sourceRoot":"","sources":["http-test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,QAA6B;IAC1D,IAAI,CAAC,iBAAiB,EAAE,CAAC;IAEzB,IAAI,CAAC;QACH,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,IAAI,CAAC,gBAAgB,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC"} \ No newline at end of file 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/issue-fixtures.js b/test/support/issue-fixtures.js new file mode 100644 index 0000000..03ec9e3 --- /dev/null +++ b/test/support/issue-fixtures.js @@ -0,0 +1,224 @@ +import nock from 'nock'; +function isObject(value) { + return typeof value === 'object' && value !== null; +} +function matchesIssueStateLookup(body, issueNumber) { + return (isObject(body) && + typeof body.query === 'string' && + body.query.includes('query IssueStateByNumber') && + isObject(body.variables) && + body.variables.issueNumber === issueNumber); +} +function matchesMarkIssueAsDuplicate(body, duplicateId, canonicalId) { + 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, duplicateId, canonicalId) { + 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, overrides = {}) { + 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) { + 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) { + 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) { + 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) => 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 ?? []).map((name) => ({ name })), + assignees: (options.requestBody.assignees ?? []).map((login) => ({ login })), + html_url: `https://github.com/${owner}/${repo}/issues/21`, + }); +} +export function mockIssueUpdateRequest(options) { + 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) => 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) { + 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) => 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) { + 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) => matchesIssueStateLookup(body, options.issueNumber)).reply(200, { + data: { repository: { issue: options.currentIssueState } }, + }); + } + if (options.unmark) { + scope + .post('/graphql', (body) => 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) => JSON.stringify(body) === JSON.stringify(options.restUpdateBody)) + .reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)) + .post('/graphql', (body) => matchesIssueStateLookup(body, options.issueNumber)) + .reply(200, { data: { repository: { issue: options.observedIssueState ?? options.currentIssueState } } }); + } + return scope; +} +export function mockIssueSetStateDuplicateRequest(options) { + 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) => matchesIssueStateLookup(body, options.issueNumber)) + .reply(200, { data: { repository: { issue: options.currentIssueState } } }) + .post('/graphql', (body) => 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) => matchesUnmarkIssueAsDuplicate(body, options.unmark.duplicateId, options.unmark.canonicalId)) + .reply(200, { data: { unmarkIssueAsDuplicate: { clientMutationId: null } } }); + } + if (options.mark) { + scope + .post('/graphql', (body) => 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) => JSON.stringify(body) === JSON.stringify(options.restUpdateBody)) + .reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)); + } + scope + .post('/graphql', (body) => matchesIssueStateLookup(body, options.issueNumber)) + .reply(200, { data: { repository: { issue: options.observedIssueState } } }); + } + return scope; +} +//# sourceMappingURL=issue-fixtures.js.map \ No newline at end of file diff --git a/test/support/issue-fixtures.js.map b/test/support/issue-fixtures.js.map new file mode 100644 index 0000000..8cd04a8 --- /dev/null +++ b/test/support/issue-fixtures.js.map @@ -0,0 +1 @@ +{"version":3,"file":"issue-fixtures.js","sourceRoot":"","sources":["issue-fixtures.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/support/issue-fixtures.ts b/test/support/issue-fixtures.ts new file mode 100644 index 0000000..12e69cd --- /dev/null +++ b/test/support/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/support/pr-fixtures.js b/test/support/pr-fixtures.js new file mode 100644 index 0000000..914a206 --- /dev/null +++ b/test/support/pr-fixtures.js @@ -0,0 +1,136 @@ +import nock from 'nock'; +export function mockPullRequestGetRequest(options) { + 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) { + 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) => 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) { + 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) { + 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) { + 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, + }); +} +//# sourceMappingURL=pr-fixtures.js.map \ No newline at end of file diff --git a/test/support/pr-fixtures.js.map b/test/support/pr-fixtures.js.map new file mode 100644 index 0000000..880e82e --- /dev/null +++ b/test/support/pr-fixtures.js.map @@ -0,0 +1 @@ +{"version":3,"file":"pr-fixtures.js","sourceRoot":"","sources":["pr-fixtures.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/support/pr-fixtures.ts b/test/support/pr-fixtures.ts new file mode 100644 index 0000000..ebf14a7 --- /dev/null +++ b/test/support/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/support/project-fixtures.js b/test/support/project-fixtures.js new file mode 100644 index 0000000..a3130cc --- /dev/null +++ b/test/support/project-fixtures.js @@ -0,0 +1,220 @@ +import nock from 'nock'; +function isObject(value) { + return typeof value === 'object' && value !== null; +} +function matchesProjectByOwnerAndNumber(body, options) { + 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, options) { + 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, options) { + 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, options) { + 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, options) { + 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) { + return { + hasNextPage: options?.hasNextPage ?? false, + endCursor: options?.endCursor ?? null, + }; +} +export function createProjectLookupResponse(options) { + return { + data: { + repositoryOwner: { + __typename: options.ownerType === 'user' ? 'User' : 'Organization', + projectV2: { + id: options.projectId, + }, + }, + }, + }; +} +export function createProjectItemsConnection(nodes, options) { + return { + nodes, + pageInfo: createPageInfo(options), + }; +} +export function createProjectFieldsConnection(nodes, options) { + return { + nodes, + pageInfo: createPageInfo(options), + }; +} +export function createProjectStatusFieldNode(options) { + return { + __typename: 'ProjectV2SingleSelectField', + id: options.id, + name: options.name, + ...(options.options ? { options: options.options } : {}), + }; +} +export function createProjectStatusValueNode(options) { + return { + __typename: 'ProjectV2ItemFieldSingleSelectValue', + optionId: options.optionId, + name: options.name, + field: { + __typename: 'ProjectV2SingleSelectField', + id: options.fieldId, + name: options.fieldName, + }, + }; +} +export function createProjectItemNode(options) { + 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) { + 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) => 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) { + return nock('https://api.github.com') + .post('/graphql', (body) => 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) { + return nock('https://api.github.com') + .post('/graphql', (body) => 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) { + 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) => 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) { + 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) => 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', + }, + }, + }, + }); +} +//# sourceMappingURL=project-fixtures.js.map \ No newline at end of file diff --git a/test/support/project-fixtures.js.map b/test/support/project-fixtures.js.map new file mode 100644 index 0000000..6ea9ff8 --- /dev/null +++ b/test/support/project-fixtures.js.map @@ -0,0 +1 @@ +{"version":3,"file":"project-fixtures.js","sourceRoot":"","sources":["project-fixtures.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/support/project-fixtures.ts b/test/support/project-fixtures.ts new file mode 100644 index 0000000..fc5d102 --- /dev/null +++ b/test/support/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/runtime-fixtures.js b/test/support/runtime-fixtures.js new file mode 100644 index 0000000..24c9efc --- /dev/null +++ b/test/support/runtime-fixtures.js @@ -0,0 +1,63 @@ +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 = {}) { + const config = { + configPath: repoConfigPath, + version: 1, + 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, + bots: { + greg: { + provider: 'github-app', + 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() { + return ''; +} +export function renderPrBodyContractMarker() { + return ''; +} +//# sourceMappingURL=runtime-fixtures.js.map \ No newline at end of file diff --git a/test/support/runtime-fixtures.js.map b/test/support/runtime-fixtures.js.map new file mode 100644 index 0000000..5f75c14 --- /dev/null +++ b/test/support/runtime-fixtures.js.map @@ -0,0 +1 @@ +{"version":3,"file":"runtime-fixtures.js","sourceRoot":"","sources":["runtime-fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtE,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;AAE/E,MAAM,UAAU,gBAAgB,CAAC,UAA+C,EAAE;IAChF,MAAM,MAAM,GAAG;QACb,UAAU,EAAE,cAAc;QAC1B,OAAO,EAAE,CAAU;QACnB,UAAU,EAAE;YACV,KAAK,EAAE,eAAe;YACtB,IAAI,EAAE,MAAM;YACZ,aAAa,EAAE,MAAM;SACtB;QACD,WAAW,EAAE;YACX,IAAI,EAAE,MAAM;SACb;KACF,CAAC;IAEF,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC;QACnC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO;QACL,GAAG,MAAM;QACT,QAAQ,EAAE;YACR,OAAO,EAAE;gBACP,KAAK,EAAE,eAAe;gBACtB,aAAa,EAAE,CAAC;gBAChB,eAAe,EAAE,QAAQ;aAC1B;SACF;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kCAAkC;IAChD,OAAO,gBAAgB,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,UAAU,EAAE,gBAAgB;QAC5B,OAAO,EAAE,CAAU;QACnB,IAAI,EAAE;YACJ,IAAI,EAAE;gBACJ,QAAQ,EAAE,YAAqB;gBAC/B,KAAK,EAAE,MAAM;gBACb,OAAO,EAAE,UAAU;gBACnB,cAAc,EAAE,eAAe;aAChC;SACF;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,OAAO,IAAI,mBAAmB,CAAC;QAC7B,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC,aAAa;QACvC,UAAU,EAAE,GAAG,EAAE,CAAC,WAAW;KAC9B,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,6BAA6B;IAC3C,OAAO,2DAA2D,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,0BAA0B;IACxC,OAAO,4DAA4D,CAAC;AACtE,CAAC"} \ No newline at end of file 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..e638173 --- /dev/null +++ b/test/wrapper/opencode-context.test.ts @@ -0,0 +1,75 @@ +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 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 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..aa00160 --- /dev/null +++ b/test/wrapper/path-overrides.test.ts @@ -0,0 +1,128 @@ +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, + }, + }); +}); + +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, + }, + }); +}); 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"] } From 912d0d4a3278e8b24c321033ddb0715fc66984b3 Mon Sep 17 00:00:00 2001 From: Mirza Merdovic Date: Sat, 9 May 2026 12:12:08 +0200 Subject: [PATCH 2/5] Remove generated test support artifacts --- test/support/auth-fixtures.js | 16 -- test/support/auth-fixtures.js.map | 1 - test/support/command-runtime.js | 36 ----- test/support/command-runtime.js.map | 1 - test/support/http-test.js | 12 -- test/support/http-test.js.map | 1 - test/support/issue-fixtures.js | 224 --------------------------- test/support/issue-fixtures.js.map | 1 - test/support/pr-fixtures.js | 136 ---------------- test/support/pr-fixtures.js.map | 1 - test/support/project-fixtures.js | 220 -------------------------- test/support/project-fixtures.js.map | 1 - test/support/runtime-fixtures.js | 63 -------- test/support/runtime-fixtures.js.map | 1 - 14 files changed, 714 deletions(-) delete mode 100644 test/support/auth-fixtures.js delete mode 100644 test/support/auth-fixtures.js.map delete mode 100644 test/support/command-runtime.js delete mode 100644 test/support/command-runtime.js.map delete mode 100644 test/support/http-test.js delete mode 100644 test/support/http-test.js.map delete mode 100644 test/support/issue-fixtures.js delete mode 100644 test/support/issue-fixtures.js.map delete mode 100644 test/support/pr-fixtures.js delete mode 100644 test/support/pr-fixtures.js.map delete mode 100644 test/support/project-fixtures.js delete mode 100644 test/support/project-fixtures.js.map delete mode 100644 test/support/runtime-fixtures.js delete mode 100644 test/support/runtime-fixtures.js.map diff --git a/test/support/auth-fixtures.js b/test/support/auth-fixtures.js deleted file mode 100644 index b37d3b5..0000000 --- a/test/support/auth-fixtures.js +++ /dev/null @@ -1,16 +0,0 @@ -import nock from 'nock'; -export function mockAuthTokenMintRequest(options = {}) { - 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; -} -//# sourceMappingURL=auth-fixtures.js.map \ No newline at end of file diff --git a/test/support/auth-fixtures.js.map b/test/support/auth-fixtures.js.map deleted file mode 100644 index e8e71fa..0000000 --- a/test/support/auth-fixtures.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"auth-fixtures.js","sourceRoot":"","sources":["auth-fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,UAAU,wBAAwB,CAAC,UAIrC,EAAE;IACJ,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,KAAK,IAAI,eAAe,CAAC;IACrD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC;IAE1C,MAAM,KAAK,GAAG,IAAI,CAAC,wBAAwB,CAAC,CAAC,GAAG,CAAC,UAAU,KAAK,IAAI,IAAI,eAAe,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,kBAAkB,IAAI,GAAG,EAAE;QAChI,EAAE,EAAE,EAAE;KACP,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,CAAC,kBAAkB,IAAI,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,IAAI,GAAG,EAAE;YAClF,KAAK,EAAE,SAAS;YAChB,UAAU,EAAE,sBAAsB;SACnC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"} \ No newline at end of file diff --git a/test/support/command-runtime.js b/test/support/command-runtime.js deleted file mode 100644 index 3481608..0000000 --- a/test/support/command-runtime.js +++ /dev/null @@ -1,36 +0,0 @@ -import { runOrfeCore } from '../../src/core.js'; -import { executeOrfeTool } from '../../src/wrapper.js'; -import { createAuthConfig, createGitHubClientFactory, createRepoConfig, createRepoConfigWithDefaultProject, } from './runtime-fixtures.js'; -export function createCoreDependencies(options = {}) { - return { - loadRepoConfigImpl: async () => options.repoConfig ?? createRepoConfig(), - loadAuthConfigImpl: async () => options.authConfig ?? createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - ...options.overrides, - }; -} -export async function runCoreCommand(options) { - return runOrfeCore({ - callerName: 'Greg', - command: options.command, - input: options.input, - ...options.request, - }, options.dependencies ?? createCoreDependencies({ repoConfig: options.repoConfig, authConfig: options.authConfig })); -} -export function createToolDependencies(options = {}) { - return { - loadRepoConfigImpl: async () => options.repoConfig ?? createRepoConfig(), - loadAuthConfigImpl: async () => options.authConfig ?? createAuthConfig(), - githubClientFactory: createGitHubClientFactory(), - ...options.overrides, - }; -} -export async function runToolCommand(options) { - return executeOrfeTool(options.input, { - agent: 'Greg', - cwd: '/tmp/repo', - ...options.context, - }, options.dependencies ?? createToolDependencies({ repoConfig: options.repoConfig, authConfig: options.authConfig })); -} -export { createRepoConfig, createRepoConfigWithDefaultProject, createAuthConfig }; -//# sourceMappingURL=command-runtime.js.map \ No newline at end of file diff --git a/test/support/command-runtime.js.map b/test/support/command-runtime.js.map deleted file mode 100644 index 53c3917..0000000 --- a/test/support/command-runtime.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"command-runtime.js","sourceRoot":"","sources":["command-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA6B,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAuD,MAAM,sBAAsB,CAAC;AAG5G,OAAO,EACL,gBAAgB,EAChB,yBAAyB,EACzB,gBAAgB,EAChB,kCAAkC,GACnC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,UAAU,sBAAsB,CAAC,UAInC,EAAE;IACJ,OAAO;QACL,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,mBAAmB,EAAE,yBAAyB,EAAE;QAChD,GAAG,OAAO,CAAC,SAAS;KACrB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAOpC;IACC,OAAO,WAAW,CAChB;QACE,UAAU,EAAE,MAAM;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,GAAG,OAAO,CAAC,OAAO;KACnB,EACD,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CACnH,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,UAInC,EAAE;IACJ,OAAO;QACL,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,gBAAgB,EAAE;QACxE,mBAAmB,EAAE,yBAAyB,EAAE;QAChD,GAAG,OAAO,CAAC,SAAS;KACrB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAMpC;IACC,OAAO,eAAe,CACpB,OAAO,CAAC,KAAK,EACb;QACE,KAAK,EAAE,MAAM;QACb,GAAG,EAAE,WAAW;QAChB,GAAG,OAAO,CAAC,OAAO;KACnB,EACD,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CACnH,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,kCAAkC,EAAE,gBAAgB,EAAE,CAAC"} \ No newline at end of file diff --git a/test/support/http-test.js b/test/support/http-test.js deleted file mode 100644 index 69a23fa..0000000 --- a/test/support/http-test.js +++ /dev/null @@ -1,12 +0,0 @@ -import nock from 'nock'; -export async function withNock(testBody) { - nock.disableNetConnect(); - try { - await testBody(); - } - finally { - nock.cleanAll(); - nock.enableNetConnect(); - } -} -//# sourceMappingURL=http-test.js.map \ No newline at end of file diff --git a/test/support/http-test.js.map b/test/support/http-test.js.map deleted file mode 100644 index 554362f..0000000 --- a/test/support/http-test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"http-test.js","sourceRoot":"","sources":["http-test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,QAA6B;IAC1D,IAAI,CAAC,iBAAiB,EAAE,CAAC;IAEzB,IAAI,CAAC;QACH,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,IAAI,CAAC,gBAAgB,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/test/support/issue-fixtures.js b/test/support/issue-fixtures.js deleted file mode 100644 index 03ec9e3..0000000 --- a/test/support/issue-fixtures.js +++ /dev/null @@ -1,224 +0,0 @@ -import nock from 'nock'; -function isObject(value) { - return typeof value === 'object' && value !== null; -} -function matchesIssueStateLookup(body, issueNumber) { - return (isObject(body) && - typeof body.query === 'string' && - body.query.includes('query IssueStateByNumber') && - isObject(body.variables) && - body.variables.issueNumber === issueNumber); -} -function matchesMarkIssueAsDuplicate(body, duplicateId, canonicalId) { - 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, duplicateId, canonicalId) { - 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, overrides = {}) { - 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) { - 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) { - 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) { - 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) => 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 ?? []).map((name) => ({ name })), - assignees: (options.requestBody.assignees ?? []).map((login) => ({ login })), - html_url: `https://github.com/${owner}/${repo}/issues/21`, - }); -} -export function mockIssueUpdateRequest(options) { - 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) => 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) { - 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) => 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) { - 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) => matchesIssueStateLookup(body, options.issueNumber)).reply(200, { - data: { repository: { issue: options.currentIssueState } }, - }); - } - if (options.unmark) { - scope - .post('/graphql', (body) => 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) => JSON.stringify(body) === JSON.stringify(options.restUpdateBody)) - .reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)) - .post('/graphql', (body) => matchesIssueStateLookup(body, options.issueNumber)) - .reply(200, { data: { repository: { issue: options.observedIssueState ?? options.currentIssueState } } }); - } - return scope; -} -export function mockIssueSetStateDuplicateRequest(options) { - 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) => matchesIssueStateLookup(body, options.issueNumber)) - .reply(200, { data: { repository: { issue: options.currentIssueState } } }) - .post('/graphql', (body) => 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) => matchesUnmarkIssueAsDuplicate(body, options.unmark.duplicateId, options.unmark.canonicalId)) - .reply(200, { data: { unmarkIssueAsDuplicate: { clientMutationId: null } } }); - } - if (options.mark) { - scope - .post('/graphql', (body) => 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) => JSON.stringify(body) === JSON.stringify(options.restUpdateBody)) - .reply(200, createIssueRestResponse(options.issueNumber, options.restUpdateBody)); - } - scope - .post('/graphql', (body) => matchesIssueStateLookup(body, options.issueNumber)) - .reply(200, { data: { repository: { issue: options.observedIssueState } } }); - } - return scope; -} -//# sourceMappingURL=issue-fixtures.js.map \ No newline at end of file diff --git a/test/support/issue-fixtures.js.map b/test/support/issue-fixtures.js.map deleted file mode 100644 index 8cd04a8..0000000 --- a/test/support/issue-fixtures.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"issue-fixtures.js","sourceRoot":"","sources":["issue-fixtures.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/support/pr-fixtures.js b/test/support/pr-fixtures.js deleted file mode 100644 index 914a206..0000000 --- a/test/support/pr-fixtures.js +++ /dev/null @@ -1,136 +0,0 @@ -import nock from 'nock'; -export function mockPullRequestGetRequest(options) { - 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) { - 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) => 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) { - 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) { - 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) { - 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, - }); -} -//# sourceMappingURL=pr-fixtures.js.map \ No newline at end of file diff --git a/test/support/pr-fixtures.js.map b/test/support/pr-fixtures.js.map deleted file mode 100644 index 880e82e..0000000 --- a/test/support/pr-fixtures.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"pr-fixtures.js","sourceRoot":"","sources":["pr-fixtures.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/support/project-fixtures.js b/test/support/project-fixtures.js deleted file mode 100644 index a3130cc..0000000 --- a/test/support/project-fixtures.js +++ /dev/null @@ -1,220 +0,0 @@ -import nock from 'nock'; -function isObject(value) { - return typeof value === 'object' && value !== null; -} -function matchesProjectByOwnerAndNumber(body, options) { - 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, options) { - 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, options) { - 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, options) { - 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, options) { - 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) { - return { - hasNextPage: options?.hasNextPage ?? false, - endCursor: options?.endCursor ?? null, - }; -} -export function createProjectLookupResponse(options) { - return { - data: { - repositoryOwner: { - __typename: options.ownerType === 'user' ? 'User' : 'Organization', - projectV2: { - id: options.projectId, - }, - }, - }, - }; -} -export function createProjectItemsConnection(nodes, options) { - return { - nodes, - pageInfo: createPageInfo(options), - }; -} -export function createProjectFieldsConnection(nodes, options) { - return { - nodes, - pageInfo: createPageInfo(options), - }; -} -export function createProjectStatusFieldNode(options) { - return { - __typename: 'ProjectV2SingleSelectField', - id: options.id, - name: options.name, - ...(options.options ? { options: options.options } : {}), - }; -} -export function createProjectStatusValueNode(options) { - return { - __typename: 'ProjectV2ItemFieldSingleSelectValue', - optionId: options.optionId, - name: options.name, - field: { - __typename: 'ProjectV2SingleSelectField', - id: options.fieldId, - name: options.fieldName, - }, - }; -} -export function createProjectItemNode(options) { - 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) { - 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) => 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) { - return nock('https://api.github.com') - .post('/graphql', (body) => 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) { - return nock('https://api.github.com') - .post('/graphql', (body) => 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) { - 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) => 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) { - 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) => 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', - }, - }, - }, - }); -} -//# sourceMappingURL=project-fixtures.js.map \ No newline at end of file diff --git a/test/support/project-fixtures.js.map b/test/support/project-fixtures.js.map deleted file mode 100644 index 6ea9ff8..0000000 --- a/test/support/project-fixtures.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"project-fixtures.js","sourceRoot":"","sources":["project-fixtures.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/test/support/runtime-fixtures.js b/test/support/runtime-fixtures.js deleted file mode 100644 index 24c9efc..0000000 --- a/test/support/runtime-fixtures.js +++ /dev/null @@ -1,63 +0,0 @@ -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 = {}) { - const config = { - configPath: repoConfigPath, - version: 1, - 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, - bots: { - greg: { - provider: 'github-app', - 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() { - return ''; -} -export function renderPrBodyContractMarker() { - return ''; -} -//# sourceMappingURL=runtime-fixtures.js.map \ No newline at end of file diff --git a/test/support/runtime-fixtures.js.map b/test/support/runtime-fixtures.js.map deleted file mode 100644 index 5f75c14..0000000 --- a/test/support/runtime-fixtures.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"runtime-fixtures.js","sourceRoot":"","sources":["runtime-fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtE,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;AAE/E,MAAM,UAAU,gBAAgB,CAAC,UAA+C,EAAE;IAChF,MAAM,MAAM,GAAG;QACb,UAAU,EAAE,cAAc;QAC1B,OAAO,EAAE,CAAU;QACnB,UAAU,EAAE;YACV,KAAK,EAAE,eAAe;YACtB,IAAI,EAAE,MAAM;YACZ,aAAa,EAAE,MAAM;SACtB;QACD,WAAW,EAAE;YACX,IAAI,EAAE,MAAM;SACb;KACF,CAAC;IAEF,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC;QACnC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO;QACL,GAAG,MAAM;QACT,QAAQ,EAAE;YACR,OAAO,EAAE;gBACP,KAAK,EAAE,eAAe;gBACtB,aAAa,EAAE,CAAC;gBAChB,eAAe,EAAE,QAAQ;aAC1B;SACF;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kCAAkC;IAChD,OAAO,gBAAgB,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,UAAU,EAAE,gBAAgB;QAC5B,OAAO,EAAE,CAAU;QACnB,IAAI,EAAE;YACJ,IAAI,EAAE;gBACJ,QAAQ,EAAE,YAAqB;gBAC/B,KAAK,EAAE,MAAM;gBACb,OAAO,EAAE,UAAU;gBACnB,cAAc,EAAE,eAAe;aAChC;SACF;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,OAAO,IAAI,mBAAmB,CAAC;QAC7B,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC,aAAa;QACvC,UAAU,EAAE,GAAG,EAAE,CAAC,WAAW;KAC9B,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,6BAA6B;IAC3C,OAAO,2DAA2D,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,0BAA0B;IACxC,OAAO,4DAA4D,CAAC;AACtE,CAAC"} \ No newline at end of file From f06d755f278b7b5d3df05b4a5fad2448bfeab9bf Mon Sep 17 00:00:00 2001 From: Mirza Merdovic Date: Sat, 9 May 2026 17:59:15 +0200 Subject: [PATCH 3/5] Fix duplicate and misplaced shared tests (#140) --- test/core/auth-config-loading.test.ts | 163 +++++--------------------- test/wrapper/opencode-context.test.ts | 20 ---- test/wrapper/path-overrides.test.ts | 35 ------ 3 files changed, 28 insertions(+), 190 deletions(-) diff --git a/test/core/auth-config-loading.test.ts b/test/core/auth-config-loading.test.ts index 11c0c4b..f0e9e47 100644 --- a/test/core/auth-config-loading.test.ts +++ b/test/core/auth-config-loading.test.ts @@ -3,80 +3,18 @@ import { test } from 'vitest'; import { OrfeError } from '../../src/errors.js'; import { runOrfeCore } from '../../src/core.js'; -import { mockAuthTokenMintRequest } from '../support/auth-fixtures.js'; -import { withNock } from '../support/http-test.js'; -import { createAuthConfig, createRepoConfig } from '../support/runtime-fixtures.js'; +import { createRepoConfig } from '../support/runtime-fixtures.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 { createGitHubClientFactory } = await import('../support/runtime-fixtures.js'); - 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); - }); -}); - -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 () => { +test('runOrfeCore rejects unmapped callers clearly for GitHub-backed commands', async () => { await assert.rejects( runOrfeCore( { callerName: 'Unknown Agent', - command: 'auth token', - input: { repo: 'throw-if-null/orfe' }, + command: 'issue get', + input: { issue_number: 14 }, }, { loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), }, ), (error: unknown) => { @@ -88,73 +26,13 @@ test('runOrfeCore fails clearly for auth token when the caller is unmapped', asy ); }); -test('runOrfeCore fails clearly for auth token when the installation is missing', async () => { - await withNock(async () => { - const api = mockAuthTokenMintRequest({ installationStatus: 404 }); - const { createGitHubClientFactory } = await import('../support/runtime-fixtures.js'); - - 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); - }); -}); - -test('runOrfeCore fails clearly for auth token when token minting is rejected', async () => { - await withNock(async () => { - const api = mockAuthTokenMintRequest({ tokenStatus: 403 }); - const { createGitHubClientFactory } = await import('../support/runtime-fixtures.js'); - - 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); - }); -}); - -test('runOrfeCore surfaces config failures for auth token clearly', async () => { +test('runOrfeCore surfaces auth config loading failures clearly for GitHub-backed commands', async () => { await assert.rejects( runOrfeCore( { callerName: 'Greg', - command: 'auth token', - input: { repo: 'throw-if-null/orfe' }, + command: 'issue get', + input: { issue_number: 14 }, }, { loadRepoConfigImpl: async () => createRepoConfig(), @@ -172,26 +50,42 @@ test('runOrfeCore surfaces config failures for auth token clearly', async () => ); }); -test('runOrfeCore rejects unmapped callers clearly', async () => { +test('runOrfeCore forwards explicit auth config paths into shared auth config loading', async () => { + let capturedOptions: Record | undefined; + await assert.rejects( runOrfeCore( { - callerName: 'Unknown Agent', + callerName: 'Greg', command: 'issue get', input: { issue_number: 14 }, + cwd: '/tmp/repo', + authConfigPath: '/tmp/custom-auth.json', }, { loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), + 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, 'caller_name_unmapped'); - assert.match(error.message, /Caller name "Unknown Agent" is not mapped/); + 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 () => { @@ -204,7 +98,6 @@ test('runOrfeCore rejects empty caller names clearly', async () => { }, { loadRepoConfigImpl: async () => createRepoConfig(), - loadAuthConfigImpl: async () => createAuthConfig(), }, ), (error: unknown) => { diff --git a/test/wrapper/opencode-context.test.ts b/test/wrapper/opencode-context.test.ts index e638173..0fb29d5 100644 --- a/test/wrapper/opencode-context.test.ts +++ b/test/wrapper/opencode-context.test.ts @@ -31,26 +31,6 @@ test('executeOrfeTool rejects missing caller context clearly', async () => { }); }); -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 rejects caller_name from tool input', async () => { const result = await executeOrfeTool( { diff --git a/test/wrapper/path-overrides.test.ts b/test/wrapper/path-overrides.test.ts index aa00160..1e6e49d 100644 --- a/test/wrapper/path-overrides.test.ts +++ b/test/wrapper/path-overrides.test.ts @@ -91,38 +91,3 @@ test('executeOrfeTool returns structured config failures when forwarded config p }, }); }); - -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, - }, - }); -}); From 2419b54dd698d9461b1f33ea11cbcb3198e4af7c Mon Sep 17 00:00:00 2001 From: Mirza Merdovic Date: Sat, 9 May 2026 19:03:45 +0200 Subject: [PATCH 4/5] Move test fixtures closer to owning command groups (#140) --- src/commands/auth/token/handler.test.ts | 2 +- src/commands/issue/comment/handler.test.ts | 2 +- src/commands/issue/create/handler.test.ts | 4 +-- src/commands/issue/get/handler.test.ts | 2 +- src/commands/issue/set-state/handler.test.ts | 2 +- src/commands/issue/update/handler.test.ts | 2 +- src/commands/pr/comment/handler.test.ts | 2 +- src/commands/pr/get-or-create/handler.test.ts | 2 +- src/commands/pr/get/handler.test.ts | 2 +- src/commands/pr/reply/handler.test.ts | 2 +- src/commands/pr/submit-review/handler.test.ts | 2 +- src/commands/pr/validate/handler.test.ts | 2 +- .../project/get-status/handler.test.ts | 2 +- .../project/set-status/handler.test.ts | 2 +- .../auth-fixtures.ts => auth/fixtures.ts} | 0 test/core/plain-data-boundary.test.ts | 34 ------------------- .../issue-fixtures.ts => issue/fixtures.ts} | 0 .../pr-fixtures.ts => pr/fixtures.ts} | 0 .../fixtures.ts} | 0 19 files changed, 15 insertions(+), 49 deletions(-) rename test/{support/auth-fixtures.ts => auth/fixtures.ts} (100%) delete mode 100644 test/core/plain-data-boundary.test.ts rename test/{support/issue-fixtures.ts => issue/fixtures.ts} (100%) rename test/{support/pr-fixtures.ts => pr/fixtures.ts} (100%) rename test/{support/project-fixtures.ts => project/fixtures.ts} (100%) diff --git a/src/commands/auth/token/handler.test.ts b/src/commands/auth/token/handler.test.ts index 0123eeb..f9b3859 100644 --- a/src/commands/auth/token/handler.test.ts +++ b/src/commands/auth/token/handler.test.ts @@ -3,7 +3,7 @@ import { test } from 'vitest'; import { OrfeError } from '../../../../src/errors.js'; import { runCoreCommand, runToolCommand } from '../../../../test/support/command-runtime.js'; -import { mockAuthTokenMintRequest } from '../../../../test/support/auth-fixtures.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 () => { diff --git a/src/commands/issue/comment/handler.test.ts b/src/commands/issue/comment/handler.test.ts index 7f7e9d7..e2ef47e 100644 --- a/src/commands/issue/comment/handler.test.ts +++ b/src/commands/issue/comment/handler.test.ts @@ -3,7 +3,7 @@ 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/support/issue-fixtures.js'; +import { mockIssueCommentRequest } from '../../../../test/issue/fixtures.js'; test('runOrfeCore posts a generic issue comment and returns structured success output', async () => { await withNock(async () => { diff --git a/src/commands/issue/create/handler.test.ts b/src/commands/issue/create/handler.test.ts index 2b54ffd..25a7e80 100644 --- a/src/commands/issue/create/handler.test.ts +++ b/src/commands/issue/create/handler.test.ts @@ -4,7 +4,7 @@ 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/support/issue-fixtures.js'; +import { mockIssueCreateRequest } from '../../../../test/issue/fixtures.js'; import { createProjectFieldsConnection, createProjectItemNode, @@ -17,7 +17,7 @@ import { mockProjectLookupRequest, mockProjectStatusFieldsRequest, mockProjectStatusUpdateRequest, -} from '../../../../test/support/project-fixtures.js'; +} from '../../../../test/project/fixtures.js'; import { createRepoConfigWithDefaultProject, renderIssueBodyContractMarker, diff --git a/src/commands/issue/get/handler.test.ts b/src/commands/issue/get/handler.test.ts index bd3f127..5a8f9fd 100644 --- a/src/commands/issue/get/handler.test.ts +++ b/src/commands/issue/get/handler.test.ts @@ -3,7 +3,7 @@ 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/support/issue-fixtures.js'; +import { mockIssueGetRequest } from '../../../../test/issue/fixtures.js'; test('runOrfeCore reads an issue and returns structured success output', async () => { await withNock(async () => { diff --git a/src/commands/issue/set-state/handler.test.ts b/src/commands/issue/set-state/handler.test.ts index 4e00db5..f4d6344 100644 --- a/src/commands/issue/set-state/handler.test.ts +++ b/src/commands/issue/set-state/handler.test.ts @@ -8,7 +8,7 @@ import { createIssueStateNode, mockIssueSetStateDuplicateRequest, mockIssueSetStateRequest, -} from '../../../../test/support/issue-fixtures.js'; +} from '../../../../test/issue/fixtures.js'; test('runOrfeCore closes an issue with structured state metadata', async () => { await withNock(async () => { diff --git a/src/commands/issue/update/handler.test.ts b/src/commands/issue/update/handler.test.ts index 49e88f1..565fa29 100644 --- a/src/commands/issue/update/handler.test.ts +++ b/src/commands/issue/update/handler.test.ts @@ -3,7 +3,7 @@ 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/support/issue-fixtures.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 () => { diff --git a/src/commands/pr/comment/handler.test.ts b/src/commands/pr/comment/handler.test.ts index 1aa1abf..6df662e 100644 --- a/src/commands/pr/comment/handler.test.ts +++ b/src/commands/pr/comment/handler.test.ts @@ -4,7 +4,7 @@ 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/support/pr-fixtures.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 () => { diff --git a/src/commands/pr/get-or-create/handler.test.ts b/src/commands/pr/get-or-create/handler.test.ts index 1b3b743..befc4d6 100644 --- a/src/commands/pr/get-or-create/handler.test.ts +++ b/src/commands/pr/get-or-create/handler.test.ts @@ -4,7 +4,7 @@ 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/support/pr-fixtures.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 () => { diff --git a/src/commands/pr/get/handler.test.ts b/src/commands/pr/get/handler.test.ts index 3e68094..760be02 100644 --- a/src/commands/pr/get/handler.test.ts +++ b/src/commands/pr/get/handler.test.ts @@ -3,7 +3,7 @@ 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/support/pr-fixtures.js'; +import { mockPullRequestGetRequest } from '../../../../test/pr/fixtures.js'; test('runOrfeCore reads a pull request and returns structured success output', async () => { await withNock(async () => { diff --git a/src/commands/pr/reply/handler.test.ts b/src/commands/pr/reply/handler.test.ts index 2b0b949..17f816a 100644 --- a/src/commands/pr/reply/handler.test.ts +++ b/src/commands/pr/reply/handler.test.ts @@ -4,7 +4,7 @@ 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/support/pr-fixtures.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 () => { diff --git a/src/commands/pr/submit-review/handler.test.ts b/src/commands/pr/submit-review/handler.test.ts index 83c63ba..e563cc9 100644 --- a/src/commands/pr/submit-review/handler.test.ts +++ b/src/commands/pr/submit-review/handler.test.ts @@ -4,7 +4,7 @@ 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/support/pr-fixtures.js'; +import { mockPullRequestSubmitReviewRequest } from '../../../../test/pr/fixtures.js'; test('runOrfeCore submits a pull request review and returns structured success output', async () => { await withNock(async () => { diff --git a/src/commands/pr/validate/handler.test.ts b/src/commands/pr/validate/handler.test.ts index 8f3a4f4..21e95e7 100644 --- a/src/commands/pr/validate/handler.test.ts +++ b/src/commands/pr/validate/handler.test.ts @@ -167,7 +167,7 @@ test('runOrfeCore returns actionable PR validation failures', async () => { test('runOrfeCore fails clearly when contract validation fails', async () => { await withNock(async () => { - const api = await import('../../../../test/support/pr-fixtures.js').then(({ mockPullRequestGetOrCreateRequest }) => + const api = await import('../../../../test/pr/fixtures.js').then(({ mockPullRequestGetOrCreateRequest }) => mockPullRequestGetOrCreateRequest({ head: 'issues/orfe-59', existingPullRequests: [], diff --git a/src/commands/project/get-status/handler.test.ts b/src/commands/project/get-status/handler.test.ts index 636d32f..cc10d57 100644 --- a/src/commands/project/get-status/handler.test.ts +++ b/src/commands/project/get-status/handler.test.ts @@ -11,7 +11,7 @@ import { createProjectStatusValueNode, mockProjectGetStatusRequest, mockProjectStatusFieldsRequest, -} from '../../../../test/support/project-fixtures.js'; +} 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 () => { diff --git a/src/commands/project/set-status/handler.test.ts b/src/commands/project/set-status/handler.test.ts index 9903ca1..d5928a4 100644 --- a/src/commands/project/set-status/handler.test.ts +++ b/src/commands/project/set-status/handler.test.ts @@ -12,7 +12,7 @@ import { mockProjectGetStatusRequest, mockProjectStatusFieldsRequest, mockProjectStatusUpdateRequest, -} from '../../../../test/support/project-fixtures.js'; +} 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 () => { diff --git a/test/support/auth-fixtures.ts b/test/auth/fixtures.ts similarity index 100% rename from test/support/auth-fixtures.ts rename to test/auth/fixtures.ts diff --git a/test/core/plain-data-boundary.test.ts b/test/core/plain-data-boundary.test.ts deleted file mode 100644 index 7b3a14a..0000000 --- a/test/core/plain-data-boundary.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import assert from 'node:assert/strict'; -import { test } from 'vitest'; - -import { runCoreCommand } from '../support/command-runtime.js'; -import { mockIssueGetRequest } from '../support/issue-fixtures.js'; -import { withNock } from '../support/http-test.js'; - -test('runOrfeCore can be exercised directly with plain callerName data', 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); - }); -}); diff --git a/test/support/issue-fixtures.ts b/test/issue/fixtures.ts similarity index 100% rename from test/support/issue-fixtures.ts rename to test/issue/fixtures.ts diff --git a/test/support/pr-fixtures.ts b/test/pr/fixtures.ts similarity index 100% rename from test/support/pr-fixtures.ts rename to test/pr/fixtures.ts diff --git a/test/support/project-fixtures.ts b/test/project/fixtures.ts similarity index 100% rename from test/support/project-fixtures.ts rename to test/project/fixtures.ts From effd39e14d2b7d13171808fe50fab49b0b17b422 Mon Sep 17 00:00:00 2001 From: Mirza Merdovic Date: Sat, 9 May 2026 19:24:34 +0200 Subject: [PATCH 5/5] Fix auth config loading test setup (#140) --- test/core/auth-config-loading.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/core/auth-config-loading.test.ts b/test/core/auth-config-loading.test.ts index f0e9e47..00dbf15 100644 --- a/test/core/auth-config-loading.test.ts +++ b/test/core/auth-config-loading.test.ts @@ -3,7 +3,7 @@ import { test } from 'vitest'; import { OrfeError } from '../../src/errors.js'; import { runOrfeCore } from '../../src/core.js'; -import { createRepoConfig } from '../support/runtime-fixtures.js'; +import { createAuthConfig, createRepoConfig } from '../support/runtime-fixtures.js'; test('runOrfeCore rejects unmapped callers clearly for GitHub-backed commands', async () => { await assert.rejects( @@ -15,6 +15,7 @@ test('runOrfeCore rejects unmapped callers clearly for GitHub-backed commands', }, { loadRepoConfigImpl: async () => createRepoConfig(), + loadAuthConfigImpl: async () => createAuthConfig(), }, ), (error: unknown) => {