diff --git a/.changeset/capability-namespace-standardization.md b/.changeset/capability-namespace-standardization.md new file mode 100644 index 00000000..7dffeee7 --- /dev/null +++ b/.changeset/capability-namespace-standardization.md @@ -0,0 +1,57 @@ +--- +"@ghx-dev/core": minor +--- + +Standardize all capability IDs to consistent naming conventions. + +**Renamed capabilities:** + +- `pr.thread.*` → `pr.threads.*` (list, reply, resolve, unresolve) +- `pr.review.*` → `pr.reviews.*` (list, request, submit) +- `pr.checks.rerun_all` → `pr.checks.rerun.all` +- `pr.checks.rerun_failed` → `pr.checks.rerun.failed` +- `workflow.get` → `workflow.view` +- `workflow.dispatch.run` → `workflow.dispatch` +- `workflow.run.rerun_all` → `workflow.run.rerun.all` +- `workflow.run.rerun_failed` → `workflow.run.rerun.failed` +- `workflow.job.logs.get` → `workflow.job.logs.view` +- `project_v2.org.get` → `project_v2.org.view` +- `project_v2.user.get` → `project_v2.user.view` +- `project_v2.item.add_issue` → `project_v2.items.issue.add` +- `project_v2.item.field.update` → `project_v2.items.field.update` +- `release.get` → `release.view` +- `release.create_draft` → `release.create` +- `release.publish_draft` → `release.publish` +- `issue.labels.update` → `issue.labels.set` +- `issue.assignees.update` → `issue.assignees.set` +- `issue.relations.get` → `issue.relations.view` +- `issue.linked_prs.list` → `issue.relations.prs.list` +- `issue.parent.set` → `issue.relations.parent.set` +- `issue.parent.remove` → `issue.relations.parent.remove` +- `issue.blocked_by.add` → `issue.relations.blocked_by.add` +- `issue.blocked_by.remove` → `issue.relations.blocked_by.remove` + +**New capabilities:** + +- `issue.labels.remove` — remove specific labels from an issue +- `issue.assignees.add` — add assignees without replacing existing +- `issue.assignees.remove` — remove specific assignees +- `issue.milestone.clear` — remove milestone from an issue +- `pr.assignees.add` — add assignees to a PR +- `pr.assignees.remove` — remove assignees from a PR +- `project_v2.items.issue.remove` — remove an issue from a Projects v2 project + +**Retired capabilities:** + +- `pr.checks.failed` — merged into `pr.checks.list` (use `state: "failed"` filter) +- `check_run.annotations.list` — annotations now inline in `pr.checks.list` output +- `pr.assignees.update` — replaced by `pr.assignees.add` + `pr.assignees.remove` +- `pr.threads.composite`, `issue.triage.composite`, `issue.update.composite` — composite infrastructure removed + +**Output schema changes:** + +- All rerun capabilities now return `{ runId: integer, queued: boolean }` (normalized) +- `pr.threads.reply` output now includes `commentId` and `commentUrl` +- `issue.relations.parent.set` output now includes `updated: boolean` +- `issue.relations.blocked_by.add` output now includes `added: boolean` +- `issue.milestone.set` no longer accepts `null` (use `issue.milestone.clear` instead) diff --git a/docs/capabilities/README.md b/docs/capabilities/README.md index d147d0fa..f754d94b 100644 --- a/docs/capabilities/README.md +++ b/docs/capabilities/README.md @@ -1,6 +1,6 @@ # Capabilities Reference -Welcome to ghx's comprehensive capabilities reference. These 69 capabilities form the +Welcome to ghx's comprehensive capabilities reference. These capabilities form the core of ghx's ability to automate GitHub workflows for AI agents. Capabilities are organized by domain — from managing issues and pull requests to @@ -62,21 +62,20 @@ graph TB | `pr.merge` | Execute a pull request merge. | cli (preferred) | | `pr.merge.status` | View pull request mergeability and readiness signals. | graphql (preferred), cli (fallback) | | `pr.branch.update` | Update pull request branch with latest base changes. | cli (preferred) | -| `pr.assignees.update` | Update pull request assignees. | cli (preferred) | -| `pr.thread.list` | List pull request review threads. | graphql (preferred) | -| `pr.thread.reply` | Reply to a pull request review thread. | graphql (preferred) | -| `pr.thread.resolve` | Resolve a pull request review thread. | graphql (preferred) | -| `pr.thread.unresolve` | Unresolve a pull request review thread. | graphql (preferred) | -| `pr.review.list` | List pull request reviews. | graphql (preferred) | -| `pr.review.submit` | Submit a pull request review (approve, request changes, or comment). | graphql (preferred) | -| `pr.review.request` | Request pull request reviewers. | cli (preferred) | +| `pr.assignees.add` | Add assignees to a pull request. | cli (preferred) | +| `pr.assignees.remove` | Remove assignees from a pull request. | cli (preferred) | +| `pr.threads.list` | List pull request review threads. | graphql (preferred) | +| `pr.threads.reply` | Reply to a pull request review thread. | graphql (preferred) | +| `pr.threads.resolve` | Resolve a pull request review thread. | graphql (preferred) | +| `pr.threads.unresolve` | Unresolve a pull request review thread. | graphql (preferred) | +| `pr.reviews.list` | List pull request reviews. | graphql (preferred) | +| `pr.reviews.submit` | Submit a pull request review (approve, request changes, or comment). | graphql (preferred) | +| `pr.reviews.request` | Request pull request reviewers. | cli (preferred) | | `pr.checks.list` | List PR check statuses with summary counts. | cli (preferred) | -| `pr.checks.failed` | List failed pull request checks. | cli (preferred) | -| `pr.checks.rerun_all` | Rerun all PR workflow checks for a selected run. | cli (preferred) | -| `pr.checks.rerun_failed` | Rerun failed PR workflow checks for a selected run. | cli (preferred) | +| `pr.checks.rerun.all` | Rerun all PR workflow checks for a selected run. | cli (preferred) | +| `pr.checks.rerun.failed` | Rerun failed PR workflow checks for a selected run. | cli (preferred) | | `pr.diff.files` | List changed files in a pull request diff. | graphql (preferred) | | `pr.diff.view` | View the unified diff for a pull request. | cli (preferred) | -| `pr.threads.composite` | Batch thread reply/resolve operations in a single GraphQL call. | graphql (preferred) | | **Releases (5)** | | `release.create_draft` | Create a draft release. | cli (preferred) | | `release.get` | Get release details by tag name. | cli (preferred) | @@ -106,8 +105,6 @@ graph TB | `project_v2.items.list` | List items in a Projects v2 project. | cli (preferred) | | `project_v2.item.add_issue` | Add an issue to a Projects v2 project. | cli (preferred) | | `project_v2.item.field.update` | Update a field on a Projects v2 project item. | cli (preferred) | -| **Check Runs (1)** | -| `check_run.annotations.list` | List annotations for one check run. | cli (preferred) | ## Domain Documentation diff --git a/docs/capabilities/pull-requests.md b/docs/capabilities/pull-requests.md index 0484e30c..e3eee003 100644 --- a/docs/capabilities/pull-requests.md +++ b/docs/capabilities/pull-requests.md @@ -83,7 +83,7 @@ npx ghx run pr.list --input '{ ### Merge and Branch -#### `pr.merge.execute` +#### `pr.merge` **Description:** Execute a pull request merge. @@ -111,7 +111,7 @@ npx ghx run pr.list --input '{ **Example:** ```bash -npx ghx run pr.merge.execute --input '{ +npx ghx run pr.merge --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, @@ -155,9 +155,9 @@ npx ghx run pr.branch.update --input '{ --- -#### `pr.ready_for_review.set` +#### `pr.update` -**Description:** Mark pull request as ready for review or draft. +**Description:** Update pull request metadata (title, body, draft status). **Input:** @@ -166,25 +166,29 @@ npx ghx run pr.branch.update --input '{ | owner | string | yes | Repository owner | | name | string | yes | Repository name | | prNumber | integer | yes | PR number (1+) | -| ready | boolean | yes | true for ready, false for draft | +| title | string | no | New PR title | +| body | string | no | New PR body (markdown) | +| draft | boolean | no | true for draft, false for ready | **Output:** | Field | Type | Description | |-------|------|-------------| | prNumber | integer | PR number | -| isDraft | boolean | true if draft, false if ready | +| title | string | Updated title | +| body | string | Updated body | +| isDraft | boolean | Current draft state | **Routes:** cli (preferred) **Example:** ```bash -npx ghx run pr.ready_for_review.set --input '{ +npx ghx run pr.update --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, - "ready": true + "draft": false }' ``` @@ -192,9 +196,9 @@ npx ghx run pr.ready_for_review.set --input '{ ### Assignees and Reviewers -#### `pr.assignees.update` +#### `pr.assignees.add` -**Description:** Update pull request assignees. +**Description:** Add assignees to a pull request. **Input:** @@ -203,34 +207,66 @@ npx ghx run pr.ready_for_review.set --input '{ | owner | string | yes | Repository owner | | name | string | yes | Repository name | | prNumber | integer | yes | PR number (1+) | -| add | array | conditional | Usernames to add | -| remove | array | conditional | Usernames to remove | +| assignees | array | yes | Usernames to add (min 1) | **Output:** | Field | Type | Description | |-------|------|-------------| | prNumber | integer | PR number | -| add | array | Added usernames | -| remove | array | Removed usernames | -| updated | boolean | true if changed | +| added | array | Added usernames | **Routes:** cli (preferred) **Example:** ```bash -npx ghx run pr.assignees.update --input '{ +npx ghx run pr.assignees.add --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, - "add": ["alice", "bob"] + "assignees": ["alice", "bob"] }' ``` --- -#### `pr.reviewers.request` +#### `pr.assignees.remove` + +**Description:** Remove assignees from a pull request. + +**Input:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| owner | string | yes | Repository owner | +| name | string | yes | Repository name | +| prNumber | integer | yes | PR number (1+) | +| assignees | array | yes | Usernames to remove (min 1) | + +**Output:** + +| Field | Type | Description | +|-------|------|-------------| +| prNumber | integer | PR number | +| removed | array | Removed usernames | + +**Routes:** cli (preferred) + +**Example:** + +```bash +npx ghx run pr.assignees.remove --input '{ + "owner": "octocat", + "name": "hello-world", + "prNumber": 123, + "assignees": ["alice"] +}' +``` + +--- + +#### `pr.reviews.request` **Description:** Request pull request reviewers. @@ -256,7 +292,7 @@ npx ghx run pr.assignees.update --input '{ **Example:** ```bash -npx ghx run pr.reviewers.request --input '{ +npx ghx run pr.reviews.request --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, @@ -268,7 +304,7 @@ npx ghx run pr.reviewers.request --input '{ ### Comments and Review Threads -#### `pr.comments.list` +#### `pr.threads.list` **Description:** List pull request review threads with unresolved filtering. @@ -298,7 +334,7 @@ npx ghx run pr.reviewers.request --input '{ **Example:** ```bash -npx ghx run pr.comments.list --input '{ +npx ghx run pr.threads.list --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, @@ -309,7 +345,7 @@ npx ghx run pr.comments.list --input '{ --- -#### `pr.comment.reply` +#### `pr.threads.reply` **Description:** Reply to a pull request review thread. @@ -332,7 +368,7 @@ npx ghx run pr.comments.list --input '{ **Example:** ```bash -npx ghx run pr.comment.reply --input '{ +npx ghx run pr.threads.reply --input '{ "threadId": "PRRT_kwDODhlyV4567890", "body": "Good point. I will update the implementation." }' @@ -340,7 +376,7 @@ npx ghx run pr.comment.reply --input '{ --- -#### `pr.comment.resolve` +#### `pr.threads.resolve` **Description:** Resolve a pull request review thread. @@ -362,14 +398,14 @@ npx ghx run pr.comment.reply --input '{ **Example:** ```bash -npx ghx run pr.comment.resolve --input '{ +npx ghx run pr.threads.resolve --input '{ "threadId": "PRRT_kwDODhlyV4567890" }' ``` --- -#### `pr.comment.unresolve` +#### `pr.threads.unresolve` **Description:** Unresolve a pull request review thread. @@ -391,7 +427,7 @@ npx ghx run pr.comment.resolve --input '{ **Example:** ```bash -npx ghx run pr.comment.unresolve --input '{ +npx ghx run pr.threads.unresolve --input '{ "threadId": "PRRT_kwDODhlyV4567890" }' ``` @@ -436,9 +472,9 @@ npx ghx run pr.reviews.list --input '{ --- -#### `pr.review.submit_approve` +#### `pr.reviews.submit` -**Description:** Submit an approving pull request review. +**Description:** Submit a pull request review (approve, request changes, or comment). **Input:** @@ -447,6 +483,7 @@ npx ghx run pr.reviews.list --input '{ | owner | string | yes | Repository owner | | name | string | yes | Repository name | | prNumber | integer | yes | PR number (1+) | +| event | string | yes | Review event (APPROVE, COMMENT, REQUEST_CHANGES) | | body | string | no | Review comment body (markdown, min 1 char) | **Output:** @@ -454,104 +491,31 @@ npx ghx run pr.reviews.list --input '{ | Field | Type | Description | |-------|------|-------------| | prNumber | integer | PR number | -| event | string | APPROVE | +| event | string | Review event submitted | | submitted | boolean | true | | body | string or null | Review body | -**Routes:** cli (preferred) +**Routes:** graphql (preferred) **Example:** ```bash -npx ghx run pr.review.submit_approve --input '{ +npx ghx run pr.reviews.submit --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, + "event": "APPROVE", "body": "Looks good! Ready to merge." }' ``` --- -#### `pr.review.submit_comment` - -**Description:** Submit a comment-only pull request review. - -**Input:** - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| owner | string | yes | Repository owner | -| name | string | yes | Repository name | -| prNumber | integer | yes | PR number (1+) | -| body | string | yes | Review comment body (markdown, min 1 char) | - -**Output:** - -| Field | Type | Description | -|-------|------|-------------| -| prNumber | integer | PR number | -| event | string | COMMENT | -| submitted | boolean | true | -| body | string | Review body | - -**Routes:** cli (preferred) - -**Example:** - -```bash -npx ghx run pr.review.submit_comment --input '{ - "owner": "octocat", - "name": "hello-world", - "prNumber": 123, - "body": "Can you add tests for this feature?" -}' -``` - ---- - -#### `pr.review.submit_request_changes` - -**Description:** Submit a pull request review requesting changes. - -**Input:** - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| owner | string | yes | Repository owner | -| name | string | yes | Repository name | -| prNumber | integer | yes | PR number (1+) | -| body | string | yes | Review comment body (markdown, min 1 char) | - -**Output:** - -| Field | Type | Description | -|-------|------|-------------| -| prNumber | integer | PR number | -| event | string | REQUEST_CHANGES | -| submitted | boolean | true | -| body | string | Review body | - -**Routes:** cli (preferred) - -**Example:** - -```bash -npx ghx run pr.review.submit_request_changes --input '{ - "owner": "octocat", - "name": "hello-world", - "prNumber": 123, - "body": "Need to refactor error handling before merge." -}' -``` - ---- - ### Checks and Status -#### `pr.checks.get_failed` +#### `pr.checks.list` -**Description:** List failed pull request checks. +**Description:** List pull request check statuses with summary counts. **Input:** @@ -565,7 +529,7 @@ npx ghx run pr.review.submit_request_changes --input '{ | Field | Type | Description | |-------|------|-------------| -| items | array | Failed checks (name, state, workflow, link) | +| items | array | All checks (name, state, workflow, link) | | summary | object | Summary (total, failed, pending, passed) | **Routes:** cli (preferred) @@ -573,7 +537,7 @@ npx ghx run pr.review.submit_request_changes --input '{ **Example:** ```bash -npx ghx run pr.checks.get_failed --input '{ +npx ghx run pr.checks.list --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123 @@ -582,7 +546,7 @@ npx ghx run pr.checks.get_failed --input '{ --- -#### `pr.checks.rerun_all` +#### `pr.checks.rerun.all` **Description:** Rerun all PR workflow checks for a selected run. @@ -599,9 +563,7 @@ npx ghx run pr.checks.get_failed --input '{ | Field | Type | Description | |-------|------|-------------| -| prNumber | integer | PR number | | runId | integer | Workflow run ID | -| mode | string | all | | queued | boolean | true | **Routes:** cli (preferred) @@ -609,7 +571,7 @@ npx ghx run pr.checks.get_failed --input '{ **Example:** ```bash -npx ghx run pr.checks.rerun_all --input '{ +npx ghx run pr.checks.rerun.all --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, @@ -619,7 +581,7 @@ npx ghx run pr.checks.rerun_all --input '{ --- -#### `pr.checks.rerun_failed` +#### `pr.checks.rerun.failed` **Description:** Rerun failed PR workflow checks for a selected run. @@ -636,9 +598,7 @@ npx ghx run pr.checks.rerun_all --input '{ | Field | Type | Description | |-------|------|-------------| -| prNumber | integer | PR number | | runId | integer | Workflow run ID | -| mode | string | failed | | queued | boolean | true | **Routes:** cli (preferred) @@ -646,7 +606,7 @@ npx ghx run pr.checks.rerun_all --input '{ **Example:** ```bash -npx ghx run pr.checks.rerun_failed --input '{ +npx ghx run pr.checks.rerun.failed --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, @@ -656,42 +616,9 @@ npx ghx run pr.checks.rerun_failed --input '{ --- -#### `pr.status.checks` - -**Description:** List pull request check statuses with summary counts. - -**Input:** - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| owner | string | yes | Repository owner | -| name | string | yes | Repository name | -| prNumber | integer | yes | PR number (1+) | - -**Output:** - -| Field | Type | Description | -|-------|------|-------------| -| items | array | All checks (name, state, workflow, link) | -| summary | object | Summary (total, failed, pending, passed) | - -**Routes:** cli (preferred) - -**Example:** - -```bash -npx ghx run pr.status.checks --input '{ - "owner": "octocat", - "name": "hello-world", - "prNumber": 123 -}' -``` - ---- - ### Diff and Mergeability -#### `pr.diff.list_files` +#### `pr.diff.files` **Description:** List changed files in a pull request diff. @@ -717,7 +644,7 @@ npx ghx run pr.status.checks --input '{ **Example:** ```bash -npx ghx run pr.diff.list_files --input '{ +npx ghx run pr.diff.files --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123, @@ -759,7 +686,7 @@ npx ghx run pr.diff.view --input '{ --- -#### `pr.mergeability.view` +#### `pr.merge.status` **Description:** View pull request mergeability and readiness signals. @@ -781,12 +708,12 @@ npx ghx run pr.diff.view --input '{ | isDraft | boolean | true if draft | | state | string | PR state | -**Routes:** cli (preferred) +**Routes:** graphql (preferred), cli (fallback) **Example:** ```bash -npx ghx run pr.mergeability.view --input '{ +npx ghx run pr.merge.status --input '{ "owner": "octocat", "name": "hello-world", "prNumber": 123 diff --git a/docs/plans/2026-02-19-benchmark-scenario-composite-update-design.md b/docs/plans/2026-02-19-benchmark-scenario-composite-update-design.md new file mode 100644 index 00000000..afef551f --- /dev/null +++ b/docs/plans/2026-02-19-benchmark-scenario-composite-update-design.md @@ -0,0 +1,67 @@ +# Benchmark Scenario Update: Composite Capabilities + CI Log Diagnosis + +**Date:** 2026-02-19 +**Status:** Approved +**Scope:** `packages/benchmark/scenarios/workflows/` + +## Context + +Four PRs merged to main today introduce composite capabilities and expanded workflow tooling: + +- **#55** — Composite capabilities: `pr.threads.composite`, `issue.triage.composite`, `issue.update.composite` +- **#57** — CLI adapter split into domain modules (internal; transparent to scenarios) +- **#54** — GQL drift checks offline (tooling; transparent to scenarios) +- **#53** — GQL lazy-loaded domain modules (internal; transparent to scenarios) + +The benchmark scenarios' `expected_capabilities` arrays must reflect the new atomic→composite promotion and the expanded CI diagnosis flow. + +## Approach + +Minimal delta (Approach A): update `expected_capabilities` and prompts only. Checkpoints remain outcome-based and are unchanged. No versioned scenario variants. + +## Changes Per Scenario + +### `pr-fix-review-comments-wf-001` + +- **`expected_capabilities`:** remove `pr.thread.reply`, `pr.thread.resolve`; add `pr.threads.composite` +- **Prompt:** unchanged +- **Checkpoints:** unchanged (`all_threads_resolved` via `pr.thread.list` with `unresolvedOnly: true`) + +```json +"expected_capabilities": ["pr.view", "pr.thread.list", "pr.threads.composite"] +``` + +### `issue-triage-comment-wf-001` + +- **`expected_capabilities`:** remove `issue.labels.update`, `issue.comments.create`; add `issue.triage.composite` +- **Prompt:** unchanged +- **Checkpoints:** unchanged (`comment_added` via `issue.comments.list`) +- **Note:** `issue.triage.composite` requires `issueId` (node ID). Agent must call `issue.view` first — already in expected capabilities. + +```json +"expected_capabilities": ["issue.view", "issue.triage.composite"] +``` + +### `pr-review-comment-wf-001` + +No changes. `pr.review.submit` moving to GQL-only is transparent to the scenario. + +### `ci-diagnose-run-wf-001` + +- **`expected_capabilities`:** add `workflow.job.logs.get` +- **Prompt:** expanded to require fetching job logs and surfacing top error lines +- **Checkpoints:** unchanged — dynamic `jobId` makes a logs-specific checkpoint impractical without fixture bindings + +```json +"expected_capabilities": ["workflow.run.view", "workflow.job.logs.get"] +``` + +``` +"prompt": "Workflow run {{runId}} in {{owner}}/{{name}} has failed. Get the run details to confirm its status and conclusion, identify which job failed, and fetch that job's logs to surface the top error lines." +``` + +## Out of Scope + +- `issue.update.composite` — no existing scenario exercises multi-field issue updates; a new scenario would be needed +- Checkpoint enrichment for CI logs — requires dynamic `jobId` fixture bindings not currently supported +- Versioned scenario variants (`-wf-002`) — no regression comparison needed; composites fully replace atomics diff --git a/docs/plans/2026-02-19-benchmark-scenario-composite-update.md b/docs/plans/2026-02-19-benchmark-scenario-composite-update.md new file mode 100644 index 00000000..60d46a24 --- /dev/null +++ b/docs/plans/2026-02-19-benchmark-scenario-composite-update.md @@ -0,0 +1,183 @@ +# Benchmark Scenario Composite Update Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Update three workflow benchmark scenarios to reflect today's composite capability merges and expanded CI diagnosis scope. + +**Architecture:** Direct JSON edits to scenario files in `packages/benchmark/scenarios/workflows/`. No code changes required — `expected_capabilities` arrays are schema-validated strings with no registry lookup at load time. `pr-review-comment-wf-001` needs no changes. + +**Tech Stack:** JSON, Vitest (benchmark test suite), `pnpm --filter @ghx-dev/benchmark` + +--- + +### Task 1: Update `pr-fix-review-comments-wf-001` + +Replace atomic thread capabilities with `pr.threads.composite`. + +**Files:** +- Modify: `packages/benchmark/scenarios/workflows/pr-fix-review-comments-wf-001.json` + +**Step 1: Edit `expected_capabilities`** + +Open the file. Change: +```json +"expected_capabilities": ["pr.view", "pr.thread.list", "pr.thread.reply", "pr.thread.resolve"] +``` +To: +```json +"expected_capabilities": ["pr.view", "pr.thread.list", "pr.threads.composite"] +``` + +Everything else in the file stays exactly the same (prompt, fixture, assertions, tags). + +**Step 2: Verify the file looks correct** + +Run: +```bash +cat packages/benchmark/scenarios/workflows/pr-fix-review-comments-wf-001.json +``` +Expected: JSON with `"expected_capabilities": ["pr.view", "pr.thread.list", "pr.threads.composite"]`. + +**Step 3: Run scenario validation** + +```bash +pnpm --filter @ghx-dev/benchmark run check:scenarios +``` +Expected: exits 0 with no errors. + +**Step 4: Commit** + +```bash +git add packages/benchmark/scenarios/workflows/pr-fix-review-comments-wf-001.json +git commit -m "feat(benchmark): use pr.threads.composite in pr-fix-review-comments-wf-001" +``` + +--- + +### Task 2: Update `issue-triage-comment-wf-001` + +Replace atomic label + comment capabilities with `issue.triage.composite`. + +**Files:** +- Modify: `packages/benchmark/scenarios/workflows/issue-triage-comment-wf-001.json` + +**Step 1: Edit `expected_capabilities`** + +Open the file. Change: +```json +"expected_capabilities": ["issue.view", "issue.labels.update", "issue.comments.create"] +``` +To: +```json +"expected_capabilities": ["issue.view", "issue.triage.composite"] +``` + +Everything else stays the same. Note: `issue.triage.composite` requires a node ID (`issueId`), not the issue number. The agent must call `issue.view` first to obtain it — `issue.view` is already in `expected_capabilities`, so the agent flow is correct. + +**Step 2: Verify the file looks correct** + +```bash +cat packages/benchmark/scenarios/workflows/issue-triage-comment-wf-001.json +``` +Expected: `"expected_capabilities": ["issue.view", "issue.triage.composite"]`. + +**Step 3: Run scenario validation** + +```bash +pnpm --filter @ghx-dev/benchmark run check:scenarios +``` +Expected: exits 0. + +**Step 4: Commit** + +```bash +git add packages/benchmark/scenarios/workflows/issue-triage-comment-wf-001.json +git commit -m "feat(benchmark): use issue.triage.composite in issue-triage-comment-wf-001" +``` + +--- + +### Task 3: Update `ci-diagnose-run-wf-001` + +Expand from "view run" to "view run + fetch failed job logs." + +**Files:** +- Modify: `packages/benchmark/scenarios/workflows/ci-diagnose-run-wf-001.json` + +**Step 1: Edit `prompt` and `expected_capabilities`** + +Open the file. Change: + +```json +"prompt": "Workflow run {{runId}} in {{owner}}/{{name}} has failed. Get the run details to confirm its status and conclusion, and identify which job failed." +``` +To: +```json +"prompt": "Workflow run {{runId}} in {{owner}}/{{name}} has failed. Get the run details to confirm its status and conclusion, identify which job failed, and fetch that job's logs to surface the top error lines." +``` + +And change: +```json +"expected_capabilities": ["workflow.run.view"] +``` +To: +```json +"expected_capabilities": ["workflow.run.view", "workflow.job.logs.get"] +``` + +Everything else stays the same (fixture, assertions, tags). + +**Step 2: Verify the file looks correct** + +```bash +cat packages/benchmark/scenarios/workflows/ci-diagnose-run-wf-001.json +``` +Expected: updated prompt and `"expected_capabilities": ["workflow.run.view", "workflow.job.logs.get"]`. + +**Step 3: Run scenario validation** + +```bash +pnpm --filter @ghx-dev/benchmark run check:scenarios +``` +Expected: exits 0. + +**Step 4: Commit** + +```bash +git add packages/benchmark/scenarios/workflows/ci-diagnose-run-wf-001.json +git commit -m "feat(benchmark): expand ci-diagnose-run-wf-001 to require job log diagnosis" +``` + +--- + +### Task 4: Run full benchmark test suite + +Confirm all benchmark unit tests still pass with the updated scenario files. + +**Step 1: Run benchmark tests** + +```bash +pnpm --filter @ghx-dev/benchmark exec vitest run +``` +Expected: all tests pass. Tests to watch: +- `test/unit/scenario-schema.test.ts` — validates scenario JSON structure +- `test/unit/scenario-sets-manifest.test.ts` — checks scenario-sets.json references +- `test/unit/check-scenarios.test.ts` — validates scenario set integrity + +**Step 2: If any test fails** + +Check which test failed. Likely cause: a test fixture hard-codes an `expected_capabilities` value from one of the changed scenarios. Fix by updating the test fixture to match the new capability name. + +**Step 3: Run full CI** + +```bash +pnpm run ci --outputStyle=static +``` +Expected: exits 0 across all packages. + +**Step 4: Commit (only if Step 2 required test fixes)** + +```bash +git add packages/benchmark/test/ +git commit -m "test(benchmark): update fixtures for composite capability rename" +``` diff --git a/packages/benchmark/scenarios/workflows/ci-diagnose-run-wf-001.json b/packages/benchmark/scenarios/workflows/ci-diagnose-run-wf-001.json index bbd035fb..cb8c3144 100644 --- a/packages/benchmark/scenarios/workflows/ci-diagnose-run-wf-001.json +++ b/packages/benchmark/scenarios/workflows/ci-diagnose-run-wf-001.json @@ -2,8 +2,8 @@ "type": "workflow", "id": "ci-diagnose-run-wf-001", "name": "Diagnose a failed CI workflow run", - "prompt": "Workflow run {{runId}} in {{owner}}/{{name}} has failed. Get the run details to confirm its status and conclusion, and identify which job failed.", - "expected_capabilities": ["workflow.run.view"], + "prompt": "Workflow run {{runId}} in {{owner}}/{{name}} has failed. Get the run details to confirm its status and conclusion, identify which job failed, and fetch that job's logs to surface the top error lines.", + "expected_capabilities": ["workflow.run.view", "workflow.job.logs.view"], "timeout_ms": 180000, "allowed_retries": 1, "fixture": { diff --git a/packages/benchmark/scenarios/workflows/issue-triage-comment-wf-001.json b/packages/benchmark/scenarios/workflows/issue-triage-comment-wf-001.json index 239b6ad9..70045fe2 100644 --- a/packages/benchmark/scenarios/workflows/issue-triage-comment-wf-001.json +++ b/packages/benchmark/scenarios/workflows/issue-triage-comment-wf-001.json @@ -3,7 +3,7 @@ "id": "issue-triage-comment-wf-001", "name": "Triage issue with label and comment", "prompt": "Issue #{{issueNumber}} in {{owner}}/{{name}} needs triage. Read the issue, then add the label 'enhancement' to it, and leave a comment saying 'Triaged — tracking as enhancement.'", - "expected_capabilities": ["issue.view", "issue.labels.update", "issue.comments.create"], + "expected_capabilities": ["issue.view", "issue.labels.set", "issue.comments.create"], "timeout_ms": 180000, "allowed_retries": 1, "fixture": { diff --git a/packages/benchmark/scenarios/workflows/pr-fix-review-comments-wf-001.json b/packages/benchmark/scenarios/workflows/pr-fix-review-comments-wf-001.json index 87cafc44..7b557ab5 100644 --- a/packages/benchmark/scenarios/workflows/pr-fix-review-comments-wf-001.json +++ b/packages/benchmark/scenarios/workflows/pr-fix-review-comments-wf-001.json @@ -2,8 +2,8 @@ "type": "workflow", "id": "pr-fix-review-comments-wf-001", "name": "Fix PR review comments", - "prompt": "There are unresolved review comments on PR #{{prNumber}} in {{owner}}/{{name}}. Read the comments, understand the feedback, and resolve each thread with an appropriate reply.", - "expected_capabilities": ["pr.view", "pr.thread.list", "pr.thread.reply", "pr.thread.resolve"], + "prompt": "PR #{{prNumber}} in {{owner}}/{{name}} has unresolved review threads. Fetch the unresolved threads, then for each one reply with an appropriate response addressing the feedback and resolve it.", + "expected_capabilities": ["pr.view", "pr.threads.list", "pr.threads.reply", "pr.threads.resolve"], "timeout_ms": 180000, "allowed_retries": 1, "fixture": { @@ -21,7 +21,7 @@ "checkpoints": [ { "name": "all_threads_resolved", - "verification_task": "pr.thread.list", + "verification_task": "pr.threads.list", "verification_input": { "unresolvedOnly": true }, diff --git a/packages/core/README.md b/packages/core/README.md index 85e6f121..c5e6c904 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -152,17 +152,17 @@ console.log(explainCapability("repo.view")) const result = await tool.execute("repo.view", { owner: "aryeko", name: "ghx" }) ``` -## 66 Capabilities +## 69 Capabilities **Repository** -- `repo.view`, `repo.labels.list`, `repo.issue_types.list` **Issues** -- `issue.view`, `issue.list`, `issue.comments.list`, `issue.create`, `issue.update`, `issue.close`, `issue.reopen`, `issue.delete`, `issue.labels.update`, `issue.assignees.update`, `issue.milestone.set`, `issue.comments.create`, `issue.linked_prs.list`, `issue.relations.get`, `issue.parent.set`, `issue.parent.remove`, `issue.blocked_by.add`, `issue.blocked_by.remove` -**Pull Requests (read)** -- `pr.view`, `pr.list`, `pr.comments.list`, `pr.reviews.list`, `pr.diff.list_files`, `pr.status.checks`, `pr.checks.get_failed`, `pr.mergeability.view` +**Pull Requests (read)** -- `pr.view`, `pr.list`, `pr.threads.list`, `pr.reviews.list`, `pr.diff.files`, `pr.checks.list`, `pr.merge.status` -**Pull Requests (execute)** -- `pr.comment.reply`, `pr.comment.resolve`, `pr.comment.unresolve`, `pr.ready_for_review.set`, `pr.review.submit_approve`, `pr.review.submit_request_changes`, `pr.review.submit_comment`, `pr.merge.execute`, `pr.checks.rerun_failed`, `pr.checks.rerun_all`, `pr.reviewers.request`, `pr.assignees.update`, `pr.branch.update` +**Pull Requests (execute)** -- `pr.threads.reply`, `pr.threads.resolve`, `pr.threads.unresolve`, `pr.update`, `pr.reviews.submit`, `pr.reviews.request`, `pr.merge`, `pr.checks.rerun.failed`, `pr.checks.rerun.all`, `pr.branch.update`, `pr.assignees.add`, `pr.assignees.remove` -**CI Diagnostics** -- `check_run.annotations.list`, `workflow_runs.list`, `workflow_job.logs.get`, `workflow_job.logs.analyze` +**CI Diagnostics** -- `workflow.runs.list`, `workflow.job.logs.get` **Releases** -- `release.list`, `release.get`, `release.create_draft`, `release.update`, `release.publish_draft` diff --git a/packages/core/package.json b/packages/core/package.json index 7bd115ac..61265845 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,6 +53,7 @@ "provenance": true }, "scripts": { + "ghx": "tsx src/cli/index.ts", "build": "tsup", "format": "biome check --write .", "format:check": "biome check .", diff --git a/packages/core/skills/using-ghx/SKILL.md b/packages/core/skills/using-ghx/SKILL.md index ee195a0c..51974584 100644 --- a/packages/core/skills/using-ghx/SKILL.md +++ b/packages/core/skills/using-ghx/SKILL.md @@ -6,19 +6,9 @@ description: Execute GitHub operations via ghx — deterministic routing, normal **CRITICAL:** Use `ghx run` for ALL GitHub operations. Do not use `gh api` or any other raw `gh` commands unless no matching ghx capability exists. -## Execute +## Discovery -```bash -ghx run --input - <<'EOF' -{...} -EOF -``` - -Result: `{ ok, data, error, meta }`. Check `ok` first. If `ok=false` and `error.retryable=true`, retry once. - -## Discovery (only when needed) - -If you don't know the capability ID or required inputs, list by domain: +If you don't know the capability ID or required inputs, list by domain first: ```bash ghx capabilities list --domain pr @@ -27,14 +17,21 @@ ghx capabilities list --domain pr Domains: `repo`, `issue`, `pr`, `release`, `workflow`, `project_v2`, `check_run`. Required inputs shown in brackets (e.g. `[owner, name, prNumber]`). -Use `ghx capabilities explain ` to see full input/output schema. +Only if you need the full input/output schema for a specific capability: -## Composite Capabilities +```bash +ghx capabilities explain +``` + +## Execute + +```bash +ghx run --input - <<'EOF' +{...} +EOF +``` -When a workflow involves multiple operations on the same resource, -prefer composite capabilities (suffixed with `.composite`) over -sequential atomic calls. Check `ghx capabilities list` for available -composites — their descriptions explain what they combine. +Result: `{ ok, data, error, meta }`. Check `ok` first. If `ok=false` and `error.retryable=true`, retry once. ## Examples diff --git a/packages/core/src/core/execute/composite.ts b/packages/core/src/core/execute/composite.ts deleted file mode 100644 index 916fddce..00000000 --- a/packages/core/src/core/execute/composite.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { BatchOperationInput } from "../../gql/batch.js" -import { OPERATION_BUILDERS } from "../../gql/builders.js" -import type { CompositeConfig, CompositeStep } from "../registry/types.js" - -export type ExpandedOperation = BatchOperationInput & { - mapResponse: (raw: unknown) => unknown -} - -export function expandCompositeSteps( - composite: CompositeConfig, - input: Record, -): ExpandedOperation[] { - const operations: ExpandedOperation[] = [] - let opIndex = 0 - - // Determine iteration: if any step has foreach, iterate over that array - const foreachKey = composite.steps.find((s) => s.foreach)?.foreach - if (foreachKey !== undefined) { - const raw = input[foreachKey] - if (!Array.isArray(raw)) { - throw new Error(`Composite foreach key "${foreachKey}" must be an array, got ${typeof raw}`) - } - } - const items = foreachKey ? (input[foreachKey] as Record[]) : [input] - - for (let i = 0; i < items.length; i++) { - const item = items[i] - if (typeof item !== "object" || item === null || Array.isArray(item)) { - throw new Error(`Composite foreach item at index ${i} must be an object`) - } - - // Action-aware: if item has an `action` field, select steps that declare matching actions. - const action = item.action as string | undefined - const selectedSteps: CompositeStep[] = action - ? composite.steps.filter((step) => step.actions?.includes(action) === true) - : composite.steps - - if (action && selectedSteps.length === 0) { - throw new Error(`Invalid action "${action}" for composite item at index ${i}`) - } - - for (const step of selectedSteps) { - if ( - step.requires_any_of && - step.requires_any_of.every((field) => item[field] === undefined) - ) { - continue - } - - const capId = step.capability_id - const builder = OPERATION_BUILDERS[capId] - if (!builder) { - throw new Error(`No builder registered for capability: ${capId}`) - } - - // Map item fields to builder input via params_map - const stepInput: Record = {} - for (const [builderParam, itemField] of Object.entries(step.params_map)) { - stepInput[builderParam] = item[itemField] - } - - const built = builder.build(stepInput) - const aliasBase = capId.replace(/[^a-zA-Z0-9]/g, "_") - operations.push({ - alias: `${aliasBase}_${opIndex++}`, - mutation: built.mutation, - variables: built.variables, - mapResponse: builder.mapResponse, - }) - } - } - - return operations -} diff --git a/packages/core/src/core/execution/adapters/cli-capability-adapter.ts b/packages/core/src/core/execution/adapters/cli-capability-adapter.ts index b3d11cdb..5ef7e5fe 100644 --- a/packages/core/src/core/execution/adapters/cli-capability-adapter.ts +++ b/packages/core/src/core/execution/adapters/cli-capability-adapter.ts @@ -14,44 +14,49 @@ export type CliCapabilityId = | "issue.view" | "issue.list" | "issue.comments.list" + | "issue.labels.remove" + | "issue.assignees.add" + | "issue.assignees.remove" + | "issue.milestone.clear" | "pr.view" | "pr.list" | "pr.create" | "pr.update" | "pr.checks.list" - | "pr.checks.failed" + | "pr.checks.rerun.failed" + | "pr.checks.rerun.all" | "pr.merge.status" + | "pr.reviews.submit" | "pr.merge" - | "pr.checks.rerun_failed" - | "pr.checks.rerun_all" - | "pr.review.request" - | "pr.assignees.update" + | "pr.reviews.request" + | "pr.assignees.add" + | "pr.assignees.remove" | "pr.branch.update" | "pr.diff.view" | "pr.diff.files" - | "check_run.annotations.list" | "workflow.runs.list" | "workflow.job.logs.raw" - | "workflow.job.logs.get" + | "workflow.job.logs.view" | "workflow.list" - | "workflow.get" + | "workflow.view" | "workflow.run.view" - | "workflow.run.rerun_all" + | "workflow.run.rerun.all" | "workflow.run.cancel" | "workflow.run.artifacts.list" - | "workflow.dispatch.run" - | "workflow.run.rerun_failed" - | "project_v2.org.get" - | "project_v2.user.get" + | "workflow.dispatch" + | "workflow.run.rerun.failed" + | "project_v2.org.view" + | "project_v2.user.view" | "project_v2.fields.list" | "project_v2.items.list" - | "project_v2.item.add_issue" - | "project_v2.item.field.update" + | "project_v2.items.issue.add" + | "project_v2.items.issue.remove" + | "project_v2.items.field.update" | "release.list" - | "release.get" - | "release.create_draft" + | "release.view" + | "release.create" | "release.update" - | "release.publish_draft" + | "release.publish" export async function runCliCapability( runner: CliCommandRunner, diff --git a/packages/core/src/core/execution/adapters/cli/domains/issue.ts b/packages/core/src/core/execution/adapters/cli/domains/issue.ts index 2896d89f..ee82a833 100644 --- a/packages/core/src/core/execution/adapters/cli/domains/issue.ts +++ b/packages/core/src/core/execution/adapters/cli/domains/issue.ts @@ -335,8 +335,243 @@ export const handleIssueCommentsList: CliHandler = async (runner, params, card) } } +export const handleIssueLabelsRemove: CliHandler = async (runner, params, card) => { + try { + const owner = String(params.owner ?? "") + const name = String(params.name ?? "") + const repo = owner && name ? `${owner}/${name}` : "" + + const issueNumber = parseStrictPositiveInt(params.issueNumber) + if (issueNumber === null) { + throw new Error("Missing or invalid issueNumber for issue.labels.remove") + } + + const labels = Array.isArray(params.labels) + ? params.labels.filter( + (value): value is string => typeof value === "string" && value.trim().length > 0, + ) + : [] + if (labels.length === 0) { + throw new Error("Missing or invalid labels for issue.labels.remove") + } + + const args = [...commandTokens(card, "issue edit"), String(issueNumber)] + if (repo) { + args.push("--repo", repo) + } + args.push("--remove-label", labels.join(",")) + + const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) + + if (result.exitCode !== 0) { + const code = mapErrorToCode(result.stderr) + return normalizeError( + { + code, + message: sanitizeCliErrorMessage(result.stderr, result.exitCode), + retryable: isRetryableErrorCode(code), + details: { capabilityId: "issue.labels.remove", exitCode: result.exitCode }, + }, + "cli", + { capabilityId: "issue.labels.remove", reason: "CARD_FALLBACK" }, + ) + } + + return normalizeResult({ issueNumber, removed: labels }, "cli", { + capabilityId: "issue.labels.remove", + reason: "CARD_FALLBACK", + }) + } catch (error: unknown) { + const code = mapErrorToCode(error) + return normalizeError( + { + code, + message: error instanceof Error ? error.message : String(error), + retryable: isRetryableErrorCode(code), + }, + "cli", + { capabilityId: "issue.labels.remove", reason: "CARD_FALLBACK" }, + ) + } +} + +export const handleIssueAssigneesAdd: CliHandler = async (runner, params, card) => { + try { + const owner = String(params.owner ?? "") + const name = String(params.name ?? "") + const repo = owner && name ? `${owner}/${name}` : "" + + const issueNumber = parseStrictPositiveInt(params.issueNumber) + if (issueNumber === null) { + throw new Error("Missing or invalid issueNumber for issue.assignees.add") + } + + const assignees = Array.isArray(params.assignees) + ? params.assignees.filter( + (value): value is string => typeof value === "string" && value.trim().length > 0, + ) + : [] + if (assignees.length === 0) { + throw new Error("Missing or invalid assignees for issue.assignees.add") + } + + const args = [...commandTokens(card, "issue edit"), String(issueNumber)] + if (repo) { + args.push("--repo", repo) + } + args.push("--add-assignee", assignees.join(",")) + + const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) + + if (result.exitCode !== 0) { + const code = mapErrorToCode(result.stderr) + return normalizeError( + { + code, + message: sanitizeCliErrorMessage(result.stderr, result.exitCode), + retryable: isRetryableErrorCode(code), + details: { capabilityId: "issue.assignees.add", exitCode: result.exitCode }, + }, + "cli", + { capabilityId: "issue.assignees.add", reason: "CARD_FALLBACK" }, + ) + } + + return normalizeResult({ issueNumber, added: assignees }, "cli", { + capabilityId: "issue.assignees.add", + reason: "CARD_FALLBACK", + }) + } catch (error: unknown) { + const code = mapErrorToCode(error) + return normalizeError( + { + code, + message: error instanceof Error ? error.message : String(error), + retryable: isRetryableErrorCode(code), + }, + "cli", + { capabilityId: "issue.assignees.add", reason: "CARD_FALLBACK" }, + ) + } +} + +export const handleIssueAssigneesRemove: CliHandler = async (runner, params, card) => { + try { + const owner = String(params.owner ?? "") + const name = String(params.name ?? "") + const repo = owner && name ? `${owner}/${name}` : "" + + const issueNumber = parseStrictPositiveInt(params.issueNumber) + if (issueNumber === null) { + throw new Error("Missing or invalid issueNumber for issue.assignees.remove") + } + + const assignees = Array.isArray(params.assignees) + ? params.assignees.filter( + (value): value is string => typeof value === "string" && value.trim().length > 0, + ) + : [] + if (assignees.length === 0) { + throw new Error("Missing or invalid assignees for issue.assignees.remove") + } + + const args = [...commandTokens(card, "issue edit"), String(issueNumber)] + if (repo) { + args.push("--repo", repo) + } + args.push("--remove-assignee", assignees.join(",")) + + const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) + + if (result.exitCode !== 0) { + const code = mapErrorToCode(result.stderr) + return normalizeError( + { + code, + message: sanitizeCliErrorMessage(result.stderr, result.exitCode), + retryable: isRetryableErrorCode(code), + details: { capabilityId: "issue.assignees.remove", exitCode: result.exitCode }, + }, + "cli", + { capabilityId: "issue.assignees.remove", reason: "CARD_FALLBACK" }, + ) + } + + return normalizeResult({ issueNumber, removed: assignees }, "cli", { + capabilityId: "issue.assignees.remove", + reason: "CARD_FALLBACK", + }) + } catch (error: unknown) { + const code = mapErrorToCode(error) + return normalizeError( + { + code, + message: error instanceof Error ? error.message : String(error), + retryable: isRetryableErrorCode(code), + }, + "cli", + { capabilityId: "issue.assignees.remove", reason: "CARD_FALLBACK" }, + ) + } +} + +export const handleIssueMilestoneClear: CliHandler = async (runner, params, card) => { + try { + const owner = String(params.owner ?? "") + const name = String(params.name ?? "") + const repo = owner && name ? `${owner}/${name}` : "" + + const issueNumber = parseStrictPositiveInt(params.issueNumber) + if (issueNumber === null) { + throw new Error("Missing or invalid issueNumber for issue.milestone.clear") + } + + const args = [...commandTokens(card, "issue edit"), String(issueNumber)] + if (repo) { + args.push("--repo", repo) + } + args.push("--milestone", "") + + const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) + + if (result.exitCode !== 0) { + const code = mapErrorToCode(result.stderr) + return normalizeError( + { + code, + message: sanitizeCliErrorMessage(result.stderr, result.exitCode), + retryable: isRetryableErrorCode(code), + details: { capabilityId: "issue.milestone.clear", exitCode: result.exitCode }, + }, + "cli", + { capabilityId: "issue.milestone.clear", reason: "CARD_FALLBACK" }, + ) + } + + return normalizeResult({ issueNumber, cleared: true }, "cli", { + capabilityId: "issue.milestone.clear", + reason: "CARD_FALLBACK", + }) + } catch (error: unknown) { + const code = mapErrorToCode(error) + return normalizeError( + { + code, + message: error instanceof Error ? error.message : String(error), + retryable: isRetryableErrorCode(code), + }, + "cli", + { capabilityId: "issue.milestone.clear", reason: "CARD_FALLBACK" }, + ) + } +} + export const handlers: Record = { "issue.view": handleIssueView, "issue.list": handleIssueList, "issue.comments.list": handleIssueCommentsList, + "issue.labels.remove": handleIssueLabelsRemove, + "issue.assignees.add": handleIssueAssigneesAdd, + "issue.assignees.remove": handleIssueAssigneesRemove, + "issue.milestone.clear": handleIssueMilestoneClear, } diff --git a/packages/core/src/core/execution/adapters/cli/domains/pr.ts b/packages/core/src/core/execution/adapters/cli/domains/pr.ts index 18494e0d..28632e42 100644 --- a/packages/core/src/core/execution/adapters/cli/domains/pr.ts +++ b/packages/core/src/core/execution/adapters/cli/domains/pr.ts @@ -324,9 +324,24 @@ const handlePrChecksList: CliHandler = async (runner, params, card) => { const failed = checks.filter((entry) => isCheckFailureBucket(entry.bucket)) const pending = checks.filter((entry) => isCheckPendingBucket(entry.bucket)) const passed = checks.filter((entry) => isCheckPassBucket(entry.bucket)) + + const state = typeof params.state === "string" ? params.state : undefined + const filteredItems = + state === "failed" + ? failed + : state === "pending" + ? pending + : state === "passed" + ? passed + : checks + const itemsWithAnnotations = filteredItems.map(({ bucket: _bucket, ...rest }) => ({ + ...rest, + annotations: [], + })) + return normalizeResult( { - items: checks, + items: itemsWithAnnotations, summary: { total: checks.length, failed: failed.length, @@ -357,71 +372,6 @@ const handlePrChecksList: CliHandler = async (runner, params, card) => { } } -const handlePrChecksFailed: CliHandler = async (runner, params, card) => { - try { - const owner = String(params.owner ?? "") - const name = String(params.name ?? "") - const repo = owner && name ? `${owner}/${name}` : "" - const prNumber = parseStrictPositiveInt(params.prNumber) - if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.checks.failed") - - const args = [...commandTokens(card, "pr checks"), String(prNumber)] - if (repo) args.push("--repo", repo) - args.push("--json", jsonFieldsFromCard(card, "name,state,bucket,workflow,link")) - - const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) - if (result.exitCode !== 0) { - const code = mapErrorToCode(result.stderr) - return normalizeError( - { - code, - message: sanitizeCliErrorMessage(result.stderr, result.exitCode), - retryable: isRetryableErrorCode(code), - details: { capabilityId: "pr.checks.failed", exitCode: result.exitCode }, - }, - "cli", - { capabilityId: "pr.checks.failed", reason: "CARD_FALLBACK" }, - ) - } - - const data = parseCliData(result.stdout) - const checks = Array.isArray(data) ? data.map((entry) => normalizeCheckItem(entry)) : [] - const failed = checks.filter((entry) => isCheckFailureBucket(entry.bucket)) - const pending = checks.filter((entry) => isCheckPendingBucket(entry.bucket)) - const passed = checks.filter((entry) => isCheckPassBucket(entry.bucket)) - return normalizeResult( - { - items: failed, - summary: { - total: checks.length, - failed: failed.length, - pending: pending.length, - passed: passed.length, - }, - }, - "cli", - { capabilityId: "pr.checks.failed", reason: "CARD_FALLBACK" }, - ) - } catch (error: unknown) { - if (error instanceof SyntaxError) - return normalizeError( - { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, - "cli", - { capabilityId: "pr.checks.failed", reason: "CARD_FALLBACK" }, - ) - const code = mapErrorToCode(error) - return normalizeError( - { - code, - message: error instanceof Error ? error.message : String(error), - retryable: isRetryableErrorCode(code), - }, - "cli", - { capabilityId: "pr.checks.failed", reason: "CARD_FALLBACK" }, - ) - } -} - const handlePrMergeStatus: CliHandler = async (runner, params, card) => { try { const owner = String(params.owner ?? "") @@ -495,10 +445,10 @@ const handlePrReviewSubmit: CliHandler = async (runner, params, card) => { const name = String(params.name ?? "") const repo = owner && name ? `${owner}/${name}` : "" const prNumber = parseStrictPositiveInt(params.prNumber) - if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.review.submit") + if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.reviews.submit") const event = params.event if (event !== "APPROVE" && event !== "COMMENT" && event !== "REQUEST_CHANGES") - throw new Error("Missing or invalid event for pr.review.submit") + throw new Error("Missing or invalid event for pr.reviews.submit") const args = [...commandTokens(card, "pr review"), String(prNumber)] if (repo) args.push("--repo", repo) @@ -522,10 +472,10 @@ const handlePrReviewSubmit: CliHandler = async (runner, params, card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "pr.review.submit", exitCode: result.exitCode }, + details: { capabilityId: "pr.reviews.submit", exitCode: result.exitCode }, }, "cli", - { capabilityId: "pr.review.submit", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.reviews.submit", reason: "CARD_FALLBACK" }, ) } @@ -537,7 +487,7 @@ const handlePrReviewSubmit: CliHandler = async (runner, params, card) => { body: typeof params.body === "string" ? params.body : null, }, "cli", - { capabilityId: "pr.review.submit", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.reviews.submit", reason: "CARD_FALLBACK" }, ) } catch (error: unknown) { const code = mapErrorToCode(error) @@ -548,7 +498,7 @@ const handlePrReviewSubmit: CliHandler = async (runner, params, card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "pr.review.submit", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.reviews.submit", reason: "CARD_FALLBACK" }, ) } } @@ -616,9 +566,9 @@ const handlePrChecksRerunFailed: CliHandler = async (runner, params, card) => { const name = String(params.name ?? "") const repo = owner && name ? `${owner}/${name}` : "" const prNumber = parseStrictPositiveInt(params.prNumber) - if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.checks.rerun_failed") + if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.checks.rerun.failed") const runId = parseStrictPositiveInt(params.runId) - if (runId === null) throw new Error("Missing or invalid runId for pr.checks.rerun_failed") + if (runId === null) throw new Error("Missing or invalid runId for pr.checks.rerun.failed") const args = [...commandTokens(card, "run rerun"), String(runId)] if (repo) args.push("--repo", repo) @@ -631,16 +581,10 @@ const handlePrChecksRerunFailed: CliHandler = async (runner, params, card) => { if (repo) rerunAllArgs.push("--repo", repo) const rerunAllResult = await runner.run("gh", rerunAllArgs, DEFAULT_TIMEOUT_MS) if (rerunAllResult.exitCode === 0) { - return normalizeResult( - { - prNumber: Number(params.prNumber), - runId: Number(params.runId), - mode: "all", - queued: true, - }, - "cli", - { capabilityId: "pr.checks.rerun_failed", reason: "CARD_FALLBACK" }, - ) + return normalizeResult({ runId, queued: true }, "cli", { + capabilityId: "pr.checks.rerun.failed", + reason: "CARD_FALLBACK", + }) } const failureStderr = rerunAllResult.stderr || rerunAllResult.stdout || result.stderr const code = mapErrorToCode(failureStderr) @@ -649,10 +593,10 @@ const handlePrChecksRerunFailed: CliHandler = async (runner, params, card) => { code, message: sanitizeCliErrorMessage(failureStderr, rerunAllResult.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "pr.checks.rerun_failed", exitCode: rerunAllResult.exitCode }, + details: { capabilityId: "pr.checks.rerun.failed", exitCode: rerunAllResult.exitCode }, }, "cli", - { capabilityId: "pr.checks.rerun_failed", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.checks.rerun.failed", reason: "CARD_FALLBACK" }, ) } const code = mapErrorToCode(result.stderr) @@ -661,23 +605,17 @@ const handlePrChecksRerunFailed: CliHandler = async (runner, params, card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "pr.checks.rerun_failed", exitCode: result.exitCode }, + details: { capabilityId: "pr.checks.rerun.failed", exitCode: result.exitCode }, }, "cli", - { capabilityId: "pr.checks.rerun_failed", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.checks.rerun.failed", reason: "CARD_FALLBACK" }, ) } - return normalizeResult( - { - prNumber: Number(params.prNumber), - runId: Number(params.runId), - mode: "failed", - queued: true, - }, - "cli", - { capabilityId: "pr.checks.rerun_failed", reason: "CARD_FALLBACK" }, - ) + return normalizeResult({ runId, queued: true }, "cli", { + capabilityId: "pr.checks.rerun.failed", + reason: "CARD_FALLBACK", + }) } catch (error: unknown) { const code = mapErrorToCode(error) return normalizeError( @@ -687,7 +625,7 @@ const handlePrChecksRerunFailed: CliHandler = async (runner, params, card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "pr.checks.rerun_failed", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.checks.rerun.failed", reason: "CARD_FALLBACK" }, ) } } @@ -698,9 +636,9 @@ const handlePrChecksRerunAll: CliHandler = async (runner, params, card) => { const name = String(params.name ?? "") const repo = owner && name ? `${owner}/${name}` : "" const prNumber = parseStrictPositiveInt(params.prNumber) - if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.checks.rerun_all") + if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.checks.rerun.all") const runId = parseStrictPositiveInt(params.runId) - if (runId === null) throw new Error("Missing or invalid runId for pr.checks.rerun_all") + if (runId === null) throw new Error("Missing or invalid runId for pr.checks.rerun.all") const args = [...commandTokens(card, "run rerun"), String(runId)] if (repo) args.push("--repo", repo) @@ -713,18 +651,17 @@ const handlePrChecksRerunAll: CliHandler = async (runner, params, card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "pr.checks.rerun_all", exitCode: result.exitCode }, + details: { capabilityId: "pr.checks.rerun.all", exitCode: result.exitCode }, }, "cli", - { capabilityId: "pr.checks.rerun_all", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.checks.rerun.all", reason: "CARD_FALLBACK" }, ) } - return normalizeResult( - { prNumber: Number(params.prNumber), runId: Number(params.runId), mode: "all", queued: true }, - "cli", - { capabilityId: "pr.checks.rerun_all", reason: "CARD_FALLBACK" }, - ) + return normalizeResult({ runId, queued: true }, "cli", { + capabilityId: "pr.checks.rerun.all", + reason: "CARD_FALLBACK", + }) } catch (error: unknown) { const code = mapErrorToCode(error) return normalizeError( @@ -734,7 +671,7 @@ const handlePrChecksRerunAll: CliHandler = async (runner, params, card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "pr.checks.rerun_all", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.checks.rerun.all", reason: "CARD_FALLBACK" }, ) } } @@ -745,14 +682,14 @@ const handlePrReviewRequest: CliHandler = async (runner, params, card) => { const name = String(params.name ?? "") const repo = owner && name ? `${owner}/${name}` : "" const prNumber = parseStrictPositiveInt(params.prNumber) - if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.review.request") + if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.reviews.request") const reviewers = Array.isArray(params.reviewers) ? params.reviewers.filter( (value): value is string => typeof value === "string" && value.trim().length > 0, ) : [] if (reviewers.length === 0) - throw new Error("Missing or invalid reviewers for pr.review.request") + throw new Error("Missing or invalid reviewers for pr.reviews.request") const args = [...commandTokens(card, "pr edit"), String(prNumber)] if (repo) args.push("--repo", repo) @@ -766,15 +703,15 @@ const handlePrReviewRequest: CliHandler = async (runner, params, card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "pr.review.request", exitCode: result.exitCode }, + details: { capabilityId: "pr.reviews.request", exitCode: result.exitCode }, }, "cli", - { capabilityId: "pr.review.request", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.reviews.request", reason: "CARD_FALLBACK" }, ) } return normalizeResult({ prNumber: Number(params.prNumber), reviewers, updated: true }, "cli", { - capabilityId: "pr.review.request", + capabilityId: "pr.reviews.request", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -786,71 +723,7 @@ const handlePrReviewRequest: CliHandler = async (runner, params, card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "pr.review.request", reason: "CARD_FALLBACK" }, - ) - } -} - -const handlePrAssigneesUpdate: CliHandler = async (runner, params, card) => { - try { - const owner = String(params.owner ?? "") - const name = String(params.name ?? "") - const repo = owner && name ? `${owner}/${name}` : "" - const prNumber = parseStrictPositiveInt(params.prNumber) - if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.assignees.update") - const addAssignees = Array.isArray(params.add) - ? params.add.filter( - (value): value is string => typeof value === "string" && value.trim().length > 0, - ) - : [] - const removeAssignees = Array.isArray(params.remove) - ? params.remove.filter( - (value): value is string => typeof value === "string" && value.trim().length > 0, - ) - : [] - if (addAssignees.length === 0 && removeAssignees.length === 0) - throw new Error("Missing or invalid add/remove assignees for pr.assignees.update") - - const args = [...commandTokens(card, "pr edit"), String(prNumber)] - if (repo) args.push("--repo", repo) - if (addAssignees.length > 0) args.push("--add-assignee", addAssignees.join(",")) - if (removeAssignees.length > 0) args.push("--remove-assignee", removeAssignees.join(",")) - - const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) - if (result.exitCode !== 0) { - const code = mapErrorToCode(result.stderr) - return normalizeError( - { - code, - message: sanitizeCliErrorMessage(result.stderr, result.exitCode), - retryable: isRetryableErrorCode(code), - details: { capabilityId: "pr.assignees.update", exitCode: result.exitCode }, - }, - "cli", - { capabilityId: "pr.assignees.update", reason: "CARD_FALLBACK" }, - ) - } - - return normalizeResult( - { - prNumber: Number(params.prNumber), - add: addAssignees, - remove: removeAssignees, - updated: true, - }, - "cli", - { capabilityId: "pr.assignees.update", reason: "CARD_FALLBACK" }, - ) - } catch (error: unknown) { - const code = mapErrorToCode(error) - return normalizeError( - { - code, - message: error instanceof Error ? error.message : String(error), - retryable: isRetryableErrorCode(code), - }, - "cli", - { capabilityId: "pr.assignees.update", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.reviews.request", reason: "CARD_FALLBACK" }, ) } } @@ -997,19 +870,23 @@ const handlePrDiffFiles: CliHandler = async (runner, params, card) => { } } -const handleCheckRunAnnotationsList: CliHandler = async (runner, params, card) => { +const handlePrAssigneesAdd: CliHandler = async (runner, params, card) => { try { const owner = String(params.owner ?? "") const name = String(params.name ?? "") - if (!owner || !name) throw new Error("Missing owner/name for check_run.annotations.list") - const checkRunId = parseStrictPositiveInt(params.checkRunId) - if (checkRunId === null) - throw new Error("Missing or invalid checkRunId for check_run.annotations.list") + const repo = owner && name ? `${owner}/${name}` : "" + const prNumber = parseStrictPositiveInt(params.prNumber) + if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.assignees.add") + const assignees = Array.isArray(params.assignees) + ? params.assignees.filter( + (value): value is string => typeof value === "string" && value.trim().length > 0, + ) + : [] + if (assignees.length === 0) throw new Error("Missing or invalid assignees for pr.assignees.add") - const args = [ - ...commandTokens(card, "api"), - `repos/${owner}/${name}/check-runs/${checkRunId}/annotations`, - ] + const args = [...commandTokens(card, "pr edit"), String(prNumber)] + if (repo) args.push("--repo", repo) + args.push("--add-assignee", assignees.join(",")) const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) if (result.exitCode !== 0) { @@ -1019,51 +896,70 @@ const handleCheckRunAnnotationsList: CliHandler = async (runner, params, card) = code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "check_run.annotations.list", exitCode: result.exitCode }, + details: { capabilityId: "pr.assignees.add", exitCode: result.exitCode }, }, "cli", - { capabilityId: "check_run.annotations.list", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.assignees.add", reason: "CARD_FALLBACK" }, ) } - const data = parseCliData(result.stdout) - const annotations = Array.isArray(data) ? data : [] - return normalizeResult( + return normalizeResult({ prNumber: Number(params.prNumber), added: assignees }, "cli", { + capabilityId: "pr.assignees.add", + reason: "CARD_FALLBACK", + }) + } catch (error: unknown) { + const code = mapErrorToCode(error) + return normalizeError( { - items: annotations.map((annotation) => { - if (typeof annotation !== "object" || annotation === null || Array.isArray(annotation)) { - return { - path: null, - startLine: null, - endLine: null, - level: null, - message: null, - title: null, - details: null, - } - } - const record = annotation as Record - return { - path: typeof record.path === "string" ? record.path : null, - startLine: typeof record.start_line === "number" ? record.start_line : null, - endLine: typeof record.end_line === "number" ? record.end_line : null, - level: typeof record.annotation_level === "string" ? record.annotation_level : null, - message: typeof record.message === "string" ? record.message : null, - title: typeof record.title === "string" ? record.title : null, - details: typeof record.raw_details === "string" ? record.raw_details : null, - } - }), + code, + message: error instanceof Error ? error.message : String(error), + retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "check_run.annotations.list", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.assignees.add", reason: "CARD_FALLBACK" }, ) - } catch (error: unknown) { - if (error instanceof SyntaxError) + } +} + +const handlePrAssigneesRemove: CliHandler = async (runner, params, card) => { + try { + const owner = String(params.owner ?? "") + const name = String(params.name ?? "") + const repo = owner && name ? `${owner}/${name}` : "" + const prNumber = parseStrictPositiveInt(params.prNumber) + if (prNumber === null) throw new Error("Missing or invalid prNumber for pr.assignees.remove") + const assignees = Array.isArray(params.assignees) + ? params.assignees.filter( + (value): value is string => typeof value === "string" && value.trim().length > 0, + ) + : [] + if (assignees.length === 0) + throw new Error("Missing or invalid assignees for pr.assignees.remove") + + const args = [...commandTokens(card, "pr edit"), String(prNumber)] + if (repo) args.push("--repo", repo) + args.push("--remove-assignee", assignees.join(",")) + + const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) + if (result.exitCode !== 0) { + const code = mapErrorToCode(result.stderr) return normalizeError( - { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, + { + code, + message: sanitizeCliErrorMessage(result.stderr, result.exitCode), + retryable: isRetryableErrorCode(code), + details: { capabilityId: "pr.assignees.remove", exitCode: result.exitCode }, + }, "cli", - { capabilityId: "check_run.annotations.list", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.assignees.remove", reason: "CARD_FALLBACK" }, ) + } + + return normalizeResult({ prNumber: Number(params.prNumber), removed: assignees }, "cli", { + capabilityId: "pr.assignees.remove", + reason: "CARD_FALLBACK", + }) + } catch (error: unknown) { const code = mapErrorToCode(error) return normalizeError( { @@ -1072,7 +968,7 @@ const handleCheckRunAnnotationsList: CliHandler = async (runner, params, card) = retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "check_run.annotations.list", reason: "CARD_FALLBACK" }, + { capabilityId: "pr.assignees.remove", reason: "CARD_FALLBACK" }, ) } } @@ -1083,16 +979,15 @@ export const handlers: Record = { "pr.create": handlePrCreate, "pr.update": handlePrUpdate, "pr.checks.list": handlePrChecksList, - "pr.checks.failed": handlePrChecksFailed, "pr.merge.status": handlePrMergeStatus, - "pr.review.submit": handlePrReviewSubmit, + "pr.reviews.submit": handlePrReviewSubmit, "pr.merge": handlePrMerge, - "pr.checks.rerun_failed": handlePrChecksRerunFailed, - "pr.checks.rerun_all": handlePrChecksRerunAll, - "pr.review.request": handlePrReviewRequest, - "pr.assignees.update": handlePrAssigneesUpdate, + "pr.checks.rerun.failed": handlePrChecksRerunFailed, + "pr.checks.rerun.all": handlePrChecksRerunAll, + "pr.reviews.request": handlePrReviewRequest, + "pr.assignees.add": handlePrAssigneesAdd, + "pr.assignees.remove": handlePrAssigneesRemove, "pr.branch.update": handlePrBranchUpdate, "pr.diff.view": handlePrDiffView, "pr.diff.files": handlePrDiffFiles, - "check_run.annotations.list": handleCheckRunAnnotationsList, } diff --git a/packages/core/src/core/execution/adapters/cli/domains/project-v2.ts b/packages/core/src/core/execution/adapters/cli/domains/project-v2.ts index 06b2766e..595e50ac 100644 --- a/packages/core/src/core/execution/adapters/cli/domains/project-v2.ts +++ b/packages/core/src/core/execution/adapters/cli/domains/project-v2.ts @@ -17,12 +17,12 @@ const handleProjectV2OrgGet: CliHandler = async (runner, params, _card) => { try { const owner = parseNonEmptyString(params.org) if (owner === null) { - throw new Error("Missing or invalid org for project_v2.org.get") + throw new Error("Missing or invalid org for project_v2.org.view") } const projectNumber = parseStrictPositiveInt(params.projectNumber) if (projectNumber === null) { - throw new Error("Missing or invalid projectNumber for project_v2.org.get") + throw new Error("Missing or invalid projectNumber for project_v2.org.view") } const args = ["project", "view", String(projectNumber), "--owner", owner, "--format", "json"] @@ -36,10 +36,10 @@ const handleProjectV2OrgGet: CliHandler = async (runner, params, _card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "project_v2.org.get", exitCode: result.exitCode }, + details: { capabilityId: "project_v2.org.view", exitCode: result.exitCode }, }, "cli", - { capabilityId: "project_v2.org.get", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.org.view", reason: "CARD_FALLBACK" }, ) } @@ -47,7 +47,7 @@ const handleProjectV2OrgGet: CliHandler = async (runner, params, _card) => { const normalized = normalizeProjectV2Summary(data) return normalizeResult(normalized, "cli", { - capabilityId: "project_v2.org.get", + capabilityId: "project_v2.org.view", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -55,7 +55,7 @@ const handleProjectV2OrgGet: CliHandler = async (runner, params, _card) => { return normalizeError( { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, "cli", - { capabilityId: "project_v2.org.get", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.org.view", reason: "CARD_FALLBACK" }, ) } const code = mapErrorToCode(error) @@ -66,7 +66,7 @@ const handleProjectV2OrgGet: CliHandler = async (runner, params, _card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "project_v2.org.get", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.org.view", reason: "CARD_FALLBACK" }, ) } } @@ -75,12 +75,12 @@ const handleProjectV2UserGet: CliHandler = async (runner, params, _card) => { try { const user = parseNonEmptyString(params.user) if (user === null) { - throw new Error("Missing or invalid user for project_v2.user.get") + throw new Error("Missing or invalid user for project_v2.user.view") } const projectNumber = parseStrictPositiveInt(params.projectNumber) if (projectNumber === null) { - throw new Error("Missing or invalid projectNumber for project_v2.user.get") + throw new Error("Missing or invalid projectNumber for project_v2.user.view") } const args = ["project", "view", String(projectNumber), "--owner", user, "--format", "json"] @@ -94,10 +94,10 @@ const handleProjectV2UserGet: CliHandler = async (runner, params, _card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "project_v2.user.get", exitCode: result.exitCode }, + details: { capabilityId: "project_v2.user.view", exitCode: result.exitCode }, }, "cli", - { capabilityId: "project_v2.user.get", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.user.view", reason: "CARD_FALLBACK" }, ) } @@ -105,7 +105,7 @@ const handleProjectV2UserGet: CliHandler = async (runner, params, _card) => { const normalized = normalizeProjectV2Summary(data) return normalizeResult(normalized, "cli", { - capabilityId: "project_v2.user.get", + capabilityId: "project_v2.user.view", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -113,7 +113,7 @@ const handleProjectV2UserGet: CliHandler = async (runner, params, _card) => { return normalizeError( { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, "cli", - { capabilityId: "project_v2.user.get", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.user.view", reason: "CARD_FALLBACK" }, ) } const code = mapErrorToCode(error) @@ -124,7 +124,7 @@ const handleProjectV2UserGet: CliHandler = async (runner, params, _card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "project_v2.user.get", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.user.view", reason: "CARD_FALLBACK" }, ) } } @@ -308,7 +308,7 @@ const handleProjectV2ItemAddIssue: CliHandler = async (runner, params, _card) => const issueUrl = parseNonEmptyString(params.issueUrl) if (projectOwner === null || projectNumber === null || issueUrl === null) { throw new Error( - "Missing or invalid owner/projectNumber/issueUrl for project_v2.item.add_issue", + "Missing or invalid owner/projectNumber/issueUrl for project_v2.items.issue.add", ) } @@ -333,10 +333,10 @@ const handleProjectV2ItemAddIssue: CliHandler = async (runner, params, _card) => code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "project_v2.item.add_issue", exitCode: result.exitCode }, + details: { capabilityId: "project_v2.items.issue.add", exitCode: result.exitCode }, }, "cli", - { capabilityId: "project_v2.item.add_issue", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.items.issue.add", reason: "CARD_FALLBACK" }, ) } @@ -349,7 +349,7 @@ const handleProjectV2ItemAddIssue: CliHandler = async (runner, params, _card) => const normalized = { itemId: typeof root.id === "string" ? root.id : null, added: true } return normalizeResult(normalized, "cli", { - capabilityId: "project_v2.item.add_issue", + capabilityId: "project_v2.items.issue.add", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -357,7 +357,7 @@ const handleProjectV2ItemAddIssue: CliHandler = async (runner, params, _card) => return normalizeError( { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, "cli", - { capabilityId: "project_v2.item.add_issue", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.items.issue.add", reason: "CARD_FALLBACK" }, ) } const code = mapErrorToCode(error) @@ -368,7 +368,7 @@ const handleProjectV2ItemAddIssue: CliHandler = async (runner, params, _card) => retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "project_v2.item.add_issue", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.items.issue.add", reason: "CARD_FALLBACK" }, ) } } @@ -380,7 +380,7 @@ const handleProjectV2ItemFieldUpdate: CliHandler = async (runner, params, _card) const fieldId = parseNonEmptyString(params.fieldId) if (projectId === null || itemId === null || fieldId === null) { throw new Error( - "Missing or invalid projectId/itemId/fieldId for project_v2.item.field.update", + "Missing or invalid projectId/itemId/fieldId for project_v2.items.field.update", ) } @@ -420,7 +420,7 @@ const handleProjectV2ItemFieldUpdate: CliHandler = async (runner, params, _card) if (clear) { args.push("--clear") } else { - throw new Error("Missing field value update for project_v2.item.field.update") + throw new Error("Missing field value update for project_v2.items.field.update") } } } @@ -437,17 +437,17 @@ const handleProjectV2ItemFieldUpdate: CliHandler = async (runner, params, _card) code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "project_v2.item.field.update", exitCode: result.exitCode }, + details: { capabilityId: "project_v2.items.field.update", exitCode: result.exitCode }, }, "cli", - { capabilityId: "project_v2.item.field.update", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.items.field.update", reason: "CARD_FALLBACK" }, ) } const normalized = { itemId, updated: true } return normalizeResult(normalized, "cli", { - capabilityId: "project_v2.item.field.update", + capabilityId: "project_v2.items.field.update", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -459,16 +459,66 @@ const handleProjectV2ItemFieldUpdate: CliHandler = async (runner, params, _card) retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "project_v2.item.field.update", reason: "CARD_FALLBACK" }, + { capabilityId: "project_v2.items.field.update", reason: "CARD_FALLBACK" }, + ) + } +} + +const handleProjectV2ItemsIssueRemove: CliHandler = async (runner, params, _card) => { + try { + const owner = parseNonEmptyString(params.owner) + const projectNumber = parseStrictPositiveInt(params.projectNumber) + const itemId = parseNonEmptyString(params.itemId) + if (owner === null || projectNumber === null || itemId === null) { + throw new Error( + "Missing or invalid owner/projectNumber/itemId for project_v2.items.issue.remove", + ) + } + + const args = ["project", "item-delete", String(projectNumber), "--owner", owner, "--id", itemId] + + const result = await runner.run("gh", args, DEFAULT_TIMEOUT_MS) + + if (result.exitCode !== 0) { + const code = mapErrorToCode(result.stderr) + return normalizeError( + { + code, + message: sanitizeCliErrorMessage(result.stderr, result.exitCode), + retryable: isRetryableErrorCode(code), + details: { capabilityId: "project_v2.items.issue.remove", exitCode: result.exitCode }, + }, + "cli", + { capabilityId: "project_v2.items.issue.remove", reason: "CARD_FALLBACK" }, + ) + } + + const normalized = { itemId, removed: true } + + return normalizeResult(normalized, "cli", { + capabilityId: "project_v2.items.issue.remove", + reason: "CARD_FALLBACK", + }) + } catch (error: unknown) { + const code = mapErrorToCode(error) + return normalizeError( + { + code, + message: error instanceof Error ? error.message : String(error), + retryable: isRetryableErrorCode(code), + }, + "cli", + { capabilityId: "project_v2.items.issue.remove", reason: "CARD_FALLBACK" }, ) } } export const handlers: Record = { - "project_v2.org.get": handleProjectV2OrgGet, - "project_v2.user.get": handleProjectV2UserGet, + "project_v2.org.view": handleProjectV2OrgGet, + "project_v2.user.view": handleProjectV2UserGet, "project_v2.fields.list": handleProjectV2FieldsList, "project_v2.items.list": handleProjectV2ItemsList, - "project_v2.item.add_issue": handleProjectV2ItemAddIssue, - "project_v2.item.field.update": handleProjectV2ItemFieldUpdate, + "project_v2.items.issue.add": handleProjectV2ItemAddIssue, + "project_v2.items.field.update": handleProjectV2ItemFieldUpdate, + "project_v2.items.issue.remove": handleProjectV2ItemsIssueRemove, } diff --git a/packages/core/src/core/execution/adapters/cli/domains/release.ts b/packages/core/src/core/execution/adapters/cli/domains/release.ts index d55668e4..eddba935 100644 --- a/packages/core/src/core/execution/adapters/cli/domains/release.ts +++ b/packages/core/src/core/execution/adapters/cli/domains/release.ts @@ -112,11 +112,11 @@ export const handleReleaseGet: CliHandler = async (runner, params, card) => { const owner = String(params.owner ?? "") const name = String(params.name ?? "") - requireRepo(owner, name, "release.get") + requireRepo(owner, name, "release.view") const tagName = parseNonEmptyString(params.tagName) if (tagName === null) { - throw new Error("Missing or invalid tagName for release.get") + throw new Error("Missing or invalid tagName for release.view") } const args = [ @@ -133,10 +133,10 @@ export const handleReleaseGet: CliHandler = async (runner, params, card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "release.get", exitCode: result.exitCode }, + details: { capabilityId: "release.view", exitCode: result.exitCode }, }, "cli", - { capabilityId: "release.get", reason: "CARD_FALLBACK" }, + { capabilityId: "release.view", reason: "CARD_FALLBACK" }, ) } @@ -144,7 +144,7 @@ export const handleReleaseGet: CliHandler = async (runner, params, card) => { const normalized = normalizeRelease(data) return normalizeResult(normalized, "cli", { - capabilityId: "release.get", + capabilityId: "release.view", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -152,7 +152,7 @@ export const handleReleaseGet: CliHandler = async (runner, params, card) => { return normalizeError( { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, "cli", - { capabilityId: "release.get", reason: "CARD_FALLBACK" }, + { capabilityId: "release.view", reason: "CARD_FALLBACK" }, ) } @@ -164,7 +164,7 @@ export const handleReleaseGet: CliHandler = async (runner, params, card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "release.get", reason: "CARD_FALLBACK" }, + { capabilityId: "release.view", reason: "CARD_FALLBACK" }, ) } } @@ -174,11 +174,11 @@ export const handleReleaseCreateDraft: CliHandler = async (runner, params, card) const owner = String(params.owner ?? "") const name = String(params.name ?? "") - requireRepo(owner, name, "release.create_draft") + requireRepo(owner, name, "release.create") const tagName = parseNonEmptyString(params.tagName) if (tagName === null) { - throw new Error("Missing or invalid tagName for release.create_draft") + throw new Error("Missing or invalid tagName for release.create") } const args = [ @@ -221,10 +221,10 @@ export const handleReleaseCreateDraft: CliHandler = async (runner, params, card) code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "release.create_draft", exitCode: result.exitCode }, + details: { capabilityId: "release.create", exitCode: result.exitCode }, }, "cli", - { capabilityId: "release.create_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.create", reason: "CARD_FALLBACK" }, ) } @@ -232,7 +232,7 @@ export const handleReleaseCreateDraft: CliHandler = async (runner, params, card) const normalized = normalizeRelease(data) return normalizeResult(normalized, "cli", { - capabilityId: "release.create_draft", + capabilityId: "release.create", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -240,7 +240,7 @@ export const handleReleaseCreateDraft: CliHandler = async (runner, params, card) return normalizeError( { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, "cli", - { capabilityId: "release.create_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.create", reason: "CARD_FALLBACK" }, ) } @@ -252,7 +252,7 @@ export const handleReleaseCreateDraft: CliHandler = async (runner, params, card) retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "release.create_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.create", reason: "CARD_FALLBACK" }, ) } } @@ -270,9 +270,7 @@ export const handleReleaseUpdate: CliHandler = async (runner, params, card) => { } if (params.draft !== undefined && params.draft !== true) { - throw new Error( - "release.update only supports draft=true; use release.publish_draft to publish", - ) + throw new Error("release.update only supports draft=true; use release.publish to publish") } const args = [ @@ -367,11 +365,11 @@ export const handleReleasePublishDraft: CliHandler = async (runner, params, card const owner = String(params.owner ?? "") const name = String(params.name ?? "") - requireRepo(owner, name, "release.publish_draft") + requireRepo(owner, name, "release.publish") const releaseId = parseStrictPositiveInt(params.releaseId) if (releaseId === null) { - throw new Error("Missing or invalid releaseId for release.publish_draft") + throw new Error("Missing or invalid releaseId for release.publish") } const readArgs = [...commandTokens(card, "api"), `repos/${owner}/${name}/releases/${releaseId}`] @@ -385,10 +383,10 @@ export const handleReleasePublishDraft: CliHandler = async (runner, params, card code, message: sanitizeCliErrorMessage(readResult.stderr, readResult.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "release.publish_draft", exitCode: readResult.exitCode }, + details: { capabilityId: "release.publish", exitCode: readResult.exitCode }, }, "cli", - { capabilityId: "release.publish_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.publish", reason: "CARD_FALLBACK" }, ) } @@ -403,11 +401,11 @@ export const handleReleasePublishDraft: CliHandler = async (runner, params, card return normalizeError( { code: errorCodes.Validation, - message: "release.publish_draft requires an existing draft release", + message: "release.publish requires an existing draft release", retryable: false, }, "cli", - { capabilityId: "release.publish_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.publish", reason: "CARD_FALLBACK" }, ) } @@ -444,10 +442,10 @@ export const handleReleasePublishDraft: CliHandler = async (runner, params, card code, message: sanitizeCliErrorMessage(publishResult.stderr, publishResult.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "release.publish_draft", exitCode: publishResult.exitCode }, + details: { capabilityId: "release.publish", exitCode: publishResult.exitCode }, }, "cli", - { capabilityId: "release.publish_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.publish", reason: "CARD_FALLBACK" }, ) } @@ -456,7 +454,7 @@ export const handleReleasePublishDraft: CliHandler = async (runner, params, card const withWasDraft = { ...normalized, wasDraft: true } return normalizeResult(withWasDraft, "cli", { - capabilityId: "release.publish_draft", + capabilityId: "release.publish", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -464,7 +462,7 @@ export const handleReleasePublishDraft: CliHandler = async (runner, params, card return normalizeError( { code: errorCodes.Server, message: "Failed to parse CLI JSON output", retryable: false }, "cli", - { capabilityId: "release.publish_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.publish", reason: "CARD_FALLBACK" }, ) } @@ -476,15 +474,15 @@ export const handleReleasePublishDraft: CliHandler = async (runner, params, card retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "release.publish_draft", reason: "CARD_FALLBACK" }, + { capabilityId: "release.publish", reason: "CARD_FALLBACK" }, ) } } export const handlers: Record = { "release.list": handleReleaseList, - "release.get": handleReleaseGet, - "release.create_draft": handleReleaseCreateDraft, + "release.view": handleReleaseGet, + "release.create": handleReleaseCreateDraft, "release.update": handleReleaseUpdate, - "release.publish_draft": handleReleasePublishDraft, + "release.publish": handleReleasePublishDraft, } diff --git a/packages/core/src/core/execution/adapters/cli/domains/workflow.ts b/packages/core/src/core/execution/adapters/cli/domains/workflow.ts index c0b58264..f1dab6cb 100644 --- a/packages/core/src/core/execution/adapters/cli/domains/workflow.ts +++ b/packages/core/src/core/execution/adapters/cli/domains/workflow.ts @@ -202,7 +202,7 @@ export const handleWorkflowJobLogsGet: CliHandler = async (runner, params, card) const jobId = parseStrictPositiveInt(params.jobId) if (jobId === null) { - throw new Error("Missing or invalid jobId for workflow.job.logs.get") + throw new Error("Missing or invalid jobId for workflow.job.logs.view") } const args = commandTokens(card, "run view") @@ -221,10 +221,10 @@ export const handleWorkflowJobLogsGet: CliHandler = async (runner, params, card) code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "workflow.job.logs.get", exitCode: result.exitCode }, + details: { capabilityId: "workflow.job.logs.view", exitCode: result.exitCode }, }, "cli", - { capabilityId: "workflow.job.logs.get", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.job.logs.view", reason: "CARD_FALLBACK" }, ) } @@ -264,7 +264,7 @@ export const handleWorkflowJobLogsGet: CliHandler = async (runner, params, card) } return normalizeResult(normalized, "cli", { - capabilityId: "workflow.job.logs.get", + capabilityId: "workflow.job.logs.view", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -276,7 +276,7 @@ export const handleWorkflowJobLogsGet: CliHandler = async (runner, params, card) retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "workflow.job.logs.get", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.job.logs.view", reason: "CARD_FALLBACK" }, ) } } @@ -361,7 +361,7 @@ export const handleWorkflowGet: CliHandler = async (runner, params, card) => { (typeof params.workflowId === "number" ? String(params.workflowId) : null) if (!workflowId) { - throw new Error("Missing or invalid workflowId for workflow.get") + throw new Error("Missing or invalid workflowId for workflow.view") } const owner = String(params.owner ?? "") @@ -386,10 +386,10 @@ export const handleWorkflowGet: CliHandler = async (runner, params, card) => { code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "workflow.get", exitCode: result.exitCode }, + details: { capabilityId: "workflow.view", exitCode: result.exitCode }, }, "cli", - { capabilityId: "workflow.get", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.view", reason: "CARD_FALLBACK" }, ) } @@ -406,7 +406,7 @@ export const handleWorkflowGet: CliHandler = async (runner, params, card) => { } return normalizeResult(normalized, "cli", { - capabilityId: "workflow.get", + capabilityId: "workflow.view", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -418,7 +418,7 @@ export const handleWorkflowGet: CliHandler = async (runner, params, card) => { retryable: false, }, "cli", - { capabilityId: "workflow.get", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.view", reason: "CARD_FALLBACK" }, ) } @@ -430,7 +430,7 @@ export const handleWorkflowGet: CliHandler = async (runner, params, card) => { retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "workflow.get", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.view", reason: "CARD_FALLBACK" }, ) } } @@ -557,7 +557,7 @@ export const handleWorkflowRunRerunAll: CliHandler = async (runner, params, card try { const runId = parseStrictPositiveInt(params.runId) if (runId === null) { - throw new Error("Missing or invalid runId for workflow.run.rerun_all") + throw new Error("Missing or invalid runId for workflow.run.rerun.all") } const owner = String(params.owner ?? "") @@ -580,20 +580,20 @@ export const handleWorkflowRunRerunAll: CliHandler = async (runner, params, card code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "workflow.run.rerun_all", exitCode: result.exitCode }, + details: { capabilityId: "workflow.run.rerun.all", exitCode: result.exitCode }, }, "cli", - { capabilityId: "workflow.run.rerun_all", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.run.rerun.all", reason: "CARD_FALLBACK" }, ) } const normalized = { runId, - status: "requested", + queued: true, } return normalizeResult(normalized, "cli", { - capabilityId: "workflow.run.rerun_all", + capabilityId: "workflow.run.rerun.all", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -605,7 +605,7 @@ export const handleWorkflowRunRerunAll: CliHandler = async (runner, params, card retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "workflow.run.rerun_all", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.run.rerun.all", reason: "CARD_FALLBACK" }, ) } } @@ -768,18 +768,18 @@ export const handleWorkflowDispatchRun: CliHandler = async (runner, params, card try { const workflowId = parseNonEmptyString(params.workflowId) if (!workflowId) { - throw new Error("Missing or invalid workflowId for workflow.dispatch.run") + throw new Error("Missing or invalid workflowId for workflow.dispatch") } const ref = parseNonEmptyString(params.ref) if (!ref) { - throw new Error("Missing or invalid ref for workflow.dispatch.run") + throw new Error("Missing or invalid ref for workflow.dispatch") } const owner = String(params.owner ?? "") const name = String(params.name ?? "") - requireRepo(owner, name, "workflow.dispatch.run") + requireRepo(owner, name, "workflow.dispatch") const encodedWorkflowId = encodeURIComponent(workflowId) const args = commandTokens(card, "api") @@ -790,15 +790,15 @@ export const handleWorkflowDispatchRun: CliHandler = async (runner, params, card if (params.inputs !== undefined) { const inputs = params.inputs if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) { - throw new Error("Missing or invalid inputs for workflow.dispatch.run") + throw new Error("Missing or invalid inputs for workflow.dispatch") } const inputsObj = inputs as Record for (const [key, value] of Object.entries(inputsObj)) { if (key.trim() === "") { - throw new Error("Missing or invalid inputs for workflow.dispatch.run") + throw new Error("Missing or invalid inputs for workflow.dispatch") } if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") { - throw new Error("Missing or invalid inputs for workflow.dispatch.run") + throw new Error("Missing or invalid inputs for workflow.dispatch") } args.push("-f", `inputs[${key}]=${String(value)}`) } @@ -813,10 +813,10 @@ export const handleWorkflowDispatchRun: CliHandler = async (runner, params, card code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "workflow.dispatch.run", exitCode: result.exitCode }, + details: { capabilityId: "workflow.dispatch", exitCode: result.exitCode }, }, "cli", - { capabilityId: "workflow.dispatch.run", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.dispatch", reason: "CARD_FALLBACK" }, ) } @@ -827,7 +827,7 @@ export const handleWorkflowDispatchRun: CliHandler = async (runner, params, card } return normalizeResult(normalized, "cli", { - capabilityId: "workflow.dispatch.run", + capabilityId: "workflow.dispatch", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -839,7 +839,7 @@ export const handleWorkflowDispatchRun: CliHandler = async (runner, params, card retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "workflow.dispatch.run", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.dispatch", reason: "CARD_FALLBACK" }, ) } } @@ -849,11 +849,11 @@ export const handleWorkflowRunRerunFailed: CliHandler = async (runner, params, c const owner = String(params.owner ?? "") const name = String(params.name ?? "") - requireRepo(owner, name, "workflow.run.rerun_failed") + requireRepo(owner, name, "workflow.run.rerun.failed") const runId = parseStrictPositiveInt(params.runId) if (runId === null) { - throw new Error("Missing or invalid runId for workflow.run.rerun_failed") + throw new Error("Missing or invalid runId for workflow.run.rerun.failed") } const args = commandTokens(card, "api") @@ -869,20 +869,20 @@ export const handleWorkflowRunRerunFailed: CliHandler = async (runner, params, c code, message: sanitizeCliErrorMessage(result.stderr, result.exitCode), retryable: isRetryableErrorCode(code), - details: { capabilityId: "workflow.run.rerun_failed", exitCode: result.exitCode }, + details: { capabilityId: "workflow.run.rerun.failed", exitCode: result.exitCode }, }, "cli", - { capabilityId: "workflow.run.rerun_failed", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.run.rerun.failed", reason: "CARD_FALLBACK" }, ) } const normalized = { runId, - rerunFailed: true, + queued: true, } return normalizeResult(normalized, "cli", { - capabilityId: "workflow.run.rerun_failed", + capabilityId: "workflow.run.rerun.failed", reason: "CARD_FALLBACK", }) } catch (error: unknown) { @@ -894,7 +894,7 @@ export const handleWorkflowRunRerunFailed: CliHandler = async (runner, params, c retryable: isRetryableErrorCode(code), }, "cli", - { capabilityId: "workflow.run.rerun_failed", reason: "CARD_FALLBACK" }, + { capabilityId: "workflow.run.rerun.failed", reason: "CARD_FALLBACK" }, ) } } @@ -902,13 +902,13 @@ export const handleWorkflowRunRerunFailed: CliHandler = async (runner, params, c export const handlers: Record = { "workflow.runs.list": handleWorkflowRunsList, "workflow.job.logs.raw": handleWorkflowJobLogsRaw, - "workflow.job.logs.get": handleWorkflowJobLogsGet, + "workflow.job.logs.view": handleWorkflowJobLogsGet, "workflow.list": handleWorkflowList, - "workflow.get": handleWorkflowGet, + "workflow.view": handleWorkflowGet, "workflow.run.view": handleWorkflowRunView, - "workflow.run.rerun_all": handleWorkflowRunRerunAll, + "workflow.run.rerun.all": handleWorkflowRunRerunAll, "workflow.run.cancel": handleWorkflowRunCancel, "workflow.run.artifacts.list": handleWorkflowRunArtifactsList, - "workflow.dispatch.run": handleWorkflowDispatchRun, - "workflow.run.rerun_failed": handleWorkflowRunRerunFailed, + "workflow.dispatch": handleWorkflowDispatchRun, + "workflow.run.rerun.failed": handleWorkflowRunRerunFailed, } diff --git a/packages/core/src/core/execution/adapters/cli/helpers.ts b/packages/core/src/core/execution/adapters/cli/helpers.ts index 47af0ac5..70fc21e7 100644 --- a/packages/core/src/core/execution/adapters/cli/helpers.ts +++ b/packages/core/src/core/execution/adapters/cli/helpers.ts @@ -14,20 +14,6 @@ export const DEFAULT_TIMEOUT_MS = 10_000 export const DEFAULT_LIST_FIRST = 30 export const MAX_WORKFLOW_JOB_LOG_CHARS = 50_000 export const REDACTED_CLI_ERROR_MESSAGE = "gh command failed; stderr redacted for safety" -export const NON_JSON_STDOUT_CAPABILITIES = new Set([ - "pr.create", - "pr.update", - "pr.checks.rerun_failed", - "pr.checks.rerun_all", - "pr.review.request", - "pr.review.submit", - "pr.merge", - "pr.assignees.update", - "pr.branch.update", - "workflow.run.cancel", - "workflow.run.rerun_failed", - "workflow.run.rerun_all", -]) export const REPO_ISSUE_TYPES_GRAPHQL_QUERY = "query($owner:String!,$name:String!,$first:Int!,$after:String){repository(owner:$owner,name:$name){issueTypes(first:$first,after:$after){nodes{id name color isEnabled} pageInfo{hasNextPage endCursor}}}}" export const ISSUE_COMMENTS_GRAPHQL_QUERY = diff --git a/packages/core/src/core/registry/cards/check_run.annotations.list.yaml b/packages/core/src/core/registry/cards/check_run.annotations.list.yaml deleted file mode 100644 index c21941eb..00000000 --- a/packages/core/src/core/registry/cards/check_run.annotations.list.yaml +++ /dev/null @@ -1,35 +0,0 @@ -capability_id: check_run.annotations.list -version: "1.0.0" -description: List annotations for one check run. -input_schema: - type: object - required: [owner, name, checkRunId] - properties: - owner: { type: string, minLength: 1 } - name: { type: string, minLength: 1 } - checkRunId: { type: integer, minimum: 1 } - additionalProperties: false -output_schema: - type: object - required: [items] - properties: - items: - type: array - items: - type: object - required: [path, startLine, endLine, level, message, title, details] - properties: - path: { type: [string, "null"] } - startLine: { type: [integer, "null"] } - endLine: { type: [integer, "null"] } - level: { type: [string, "null"] } - message: { type: [string, "null"] } - title: { type: [string, "null"] } - details: { type: [string, "null"] } - additionalProperties: false - additionalProperties: false -routing: - preferred: cli - fallbacks: [] -cli: - command: api diff --git a/packages/core/src/core/registry/cards/issue.assignees.add.yaml b/packages/core/src/core/registry/cards/issue.assignees.add.yaml new file mode 100644 index 00000000..495e644e --- /dev/null +++ b/packages/core/src/core/registry/cards/issue.assignees.add.yaml @@ -0,0 +1,29 @@ +capability_id: issue.assignees.add +version: "1.0.0" +description: Add assignees to an issue without replacing existing ones. +input_schema: + type: object + required: [owner, name, issueNumber, assignees] + properties: + owner: { type: string, minLength: 1 } + name: { type: string, minLength: 1 } + issueNumber: { type: integer, minimum: 1 } + assignees: + type: array + items: { type: string, minLength: 1 } + minItems: 1 + additionalProperties: false +output_schema: + type: object + required: [issueNumber, added] + properties: + issueNumber: { type: integer, minimum: 1 } + added: + type: array + items: { type: string } + additionalProperties: false +routing: + preferred: cli + fallbacks: [] +cli: + command: issue edit diff --git a/packages/core/src/core/registry/cards/issue.assignees.remove.yaml b/packages/core/src/core/registry/cards/issue.assignees.remove.yaml new file mode 100644 index 00000000..9914c937 --- /dev/null +++ b/packages/core/src/core/registry/cards/issue.assignees.remove.yaml @@ -0,0 +1,29 @@ +capability_id: issue.assignees.remove +version: "1.0.0" +description: Remove specific assignees from an issue. +input_schema: + type: object + required: [owner, name, issueNumber, assignees] + properties: + owner: { type: string, minLength: 1 } + name: { type: string, minLength: 1 } + issueNumber: { type: integer, minimum: 1 } + assignees: + type: array + items: { type: string, minLength: 1 } + minItems: 1 + additionalProperties: false +output_schema: + type: object + required: [issueNumber, removed] + properties: + issueNumber: { type: integer, minimum: 1 } + removed: + type: array + items: { type: string, minLength: 1 } + additionalProperties: false +routing: + preferred: cli + fallbacks: [] +cli: + command: issue edit diff --git a/packages/core/src/core/registry/cards/issue.assignees.update.yaml b/packages/core/src/core/registry/cards/issue.assignees.set.yaml similarity index 94% rename from packages/core/src/core/registry/cards/issue.assignees.update.yaml rename to packages/core/src/core/registry/cards/issue.assignees.set.yaml index 4efd57ac..92be5b22 100644 --- a/packages/core/src/core/registry/cards/issue.assignees.update.yaml +++ b/packages/core/src/core/registry/cards/issue.assignees.set.yaml @@ -1,4 +1,4 @@ -capability_id: issue.assignees.update +capability_id: issue.assignees.set version: "1.0.0" description: Replace issue assignees. input_schema: diff --git a/packages/core/src/core/registry/cards/issue.labels.remove.yaml b/packages/core/src/core/registry/cards/issue.labels.remove.yaml new file mode 100644 index 00000000..ed7af6f0 --- /dev/null +++ b/packages/core/src/core/registry/cards/issue.labels.remove.yaml @@ -0,0 +1,29 @@ +capability_id: issue.labels.remove +version: "1.0.0" +description: Remove specific labels from an issue. +input_schema: + type: object + required: [owner, name, issueNumber, labels] + properties: + owner: { type: string, minLength: 1 } + name: { type: string, minLength: 1 } + issueNumber: { type: integer, minimum: 1 } + labels: + type: array + items: { type: string, minLength: 1 } + minItems: 1 + additionalProperties: false +output_schema: + type: object + required: [issueNumber, removed] + properties: + issueNumber: { type: integer, minimum: 1 } + removed: + type: array + items: { type: string } + additionalProperties: false +routing: + preferred: cli + fallbacks: [] +cli: + command: issue edit diff --git a/packages/core/src/core/registry/cards/issue.labels.update.yaml b/packages/core/src/core/registry/cards/issue.labels.set.yaml similarity index 94% rename from packages/core/src/core/registry/cards/issue.labels.update.yaml rename to packages/core/src/core/registry/cards/issue.labels.set.yaml index d83d5270..eee24ba1 100644 --- a/packages/core/src/core/registry/cards/issue.labels.update.yaml +++ b/packages/core/src/core/registry/cards/issue.labels.set.yaml @@ -1,4 +1,4 @@ -capability_id: issue.labels.update +capability_id: issue.labels.set version: "1.0.0" description: Replace issue labels. input_schema: diff --git a/packages/core/src/core/registry/cards/issue.milestone.clear.yaml b/packages/core/src/core/registry/cards/issue.milestone.clear.yaml new file mode 100644 index 00000000..59f8ce30 --- /dev/null +++ b/packages/core/src/core/registry/cards/issue.milestone.clear.yaml @@ -0,0 +1,23 @@ +capability_id: issue.milestone.clear +version: "1.0.0" +description: Remove the milestone from an issue. +input_schema: + type: object + required: [owner, name, issueNumber] + properties: + owner: { type: string, minLength: 1 } + name: { type: string, minLength: 1 } + issueNumber: { type: integer, minimum: 1 } + additionalProperties: false +output_schema: + type: object + required: [issueNumber, cleared] + properties: + issueNumber: { type: integer, minimum: 1 } + cleared: { type: boolean } + additionalProperties: false +routing: + preferred: cli + fallbacks: [] +cli: + command: issue edit diff --git a/packages/core/src/core/registry/cards/issue.milestone.set.yaml b/packages/core/src/core/registry/cards/issue.milestone.set.yaml index 1b5b4e64..45c3b1ad 100644 --- a/packages/core/src/core/registry/cards/issue.milestone.set.yaml +++ b/packages/core/src/core/registry/cards/issue.milestone.set.yaml @@ -6,14 +6,15 @@ input_schema: required: [issueId, milestoneNumber] properties: issueId: { type: string, minLength: 1 } - milestoneNumber: { type: [integer, "null"], minimum: 1 } + milestoneNumber: { type: integer, minimum: 1 } additionalProperties: false output_schema: type: object - required: [id, milestoneNumber] + required: [id, milestoneNumber, updated] properties: id: { type: string, minLength: 1 } milestoneNumber: { type: [integer, "null"] } + updated: { type: boolean } additionalProperties: false routing: preferred: graphql diff --git a/packages/core/src/core/registry/cards/issue.blocked_by.add.yaml b/packages/core/src/core/registry/cards/issue.relations.blocked_by.add.yaml similarity index 83% rename from packages/core/src/core/registry/cards/issue.blocked_by.add.yaml rename to packages/core/src/core/registry/cards/issue.relations.blocked_by.add.yaml index d154b5a6..0adb019c 100644 --- a/packages/core/src/core/registry/cards/issue.blocked_by.add.yaml +++ b/packages/core/src/core/registry/cards/issue.relations.blocked_by.add.yaml @@ -1,4 +1,4 @@ -capability_id: issue.blocked_by.add +capability_id: issue.relations.blocked_by.add version: "1.0.0" description: Add a blocked-by relation for an issue. input_schema: @@ -10,10 +10,11 @@ input_schema: additionalProperties: false output_schema: type: object - required: [issueId, blockedByIssueId] + required: [issueId, blockedByIssueId, added] properties: issueId: { type: string, minLength: 1 } blockedByIssueId: { type: string, minLength: 1 } + added: { type: boolean } additionalProperties: false routing: preferred: graphql diff --git a/packages/core/src/core/registry/cards/issue.blocked_by.remove.yaml b/packages/core/src/core/registry/cards/issue.relations.blocked_by.remove.yaml similarity index 93% rename from packages/core/src/core/registry/cards/issue.blocked_by.remove.yaml rename to packages/core/src/core/registry/cards/issue.relations.blocked_by.remove.yaml index f6453cd3..b4af19fa 100644 --- a/packages/core/src/core/registry/cards/issue.blocked_by.remove.yaml +++ b/packages/core/src/core/registry/cards/issue.relations.blocked_by.remove.yaml @@ -1,4 +1,4 @@ -capability_id: issue.blocked_by.remove +capability_id: issue.relations.blocked_by.remove version: "1.0.0" description: Remove a blocked-by relation for an issue. input_schema: diff --git a/packages/core/src/core/registry/cards/issue.parent.remove.yaml b/packages/core/src/core/registry/cards/issue.relations.parent.remove.yaml similarity index 92% rename from packages/core/src/core/registry/cards/issue.parent.remove.yaml rename to packages/core/src/core/registry/cards/issue.relations.parent.remove.yaml index dbde8f0d..ba5a98e3 100644 --- a/packages/core/src/core/registry/cards/issue.parent.remove.yaml +++ b/packages/core/src/core/registry/cards/issue.relations.parent.remove.yaml @@ -1,4 +1,4 @@ -capability_id: issue.parent.remove +capability_id: issue.relations.parent.remove version: "1.0.0" description: Remove an issue parent relation. input_schema: diff --git a/packages/core/src/core/registry/cards/issue.parent.set.yaml b/packages/core/src/core/registry/cards/issue.relations.parent.set.yaml similarity index 82% rename from packages/core/src/core/registry/cards/issue.parent.set.yaml rename to packages/core/src/core/registry/cards/issue.relations.parent.set.yaml index 72d426b6..b2680263 100644 --- a/packages/core/src/core/registry/cards/issue.parent.set.yaml +++ b/packages/core/src/core/registry/cards/issue.relations.parent.set.yaml @@ -1,4 +1,4 @@ -capability_id: issue.parent.set +capability_id: issue.relations.parent.set version: "1.0.0" description: Set an issue parent relation. input_schema: @@ -10,10 +10,11 @@ input_schema: additionalProperties: false output_schema: type: object - required: [issueId, parentIssueId] + required: [issueId, parentIssueId, updated] properties: issueId: { type: string, minLength: 1 } parentIssueId: { type: string, minLength: 1 } + updated: { type: boolean } additionalProperties: false routing: preferred: graphql diff --git a/packages/core/src/core/registry/cards/issue.linked_prs.list.yaml b/packages/core/src/core/registry/cards/issue.relations.prs.list.yaml similarity index 95% rename from packages/core/src/core/registry/cards/issue.linked_prs.list.yaml rename to packages/core/src/core/registry/cards/issue.relations.prs.list.yaml index 9b012a5f..c7e485dd 100644 --- a/packages/core/src/core/registry/cards/issue.linked_prs.list.yaml +++ b/packages/core/src/core/registry/cards/issue.relations.prs.list.yaml @@ -1,4 +1,4 @@ -capability_id: issue.linked_prs.list +capability_id: issue.relations.prs.list version: "1.0.0" description: List pull requests linked to an issue. input_schema: diff --git a/packages/core/src/core/registry/cards/issue.relations.get.yaml b/packages/core/src/core/registry/cards/issue.relations.view.yaml similarity index 97% rename from packages/core/src/core/registry/cards/issue.relations.get.yaml rename to packages/core/src/core/registry/cards/issue.relations.view.yaml index 6fbf3a5c..11107a7a 100644 --- a/packages/core/src/core/registry/cards/issue.relations.get.yaml +++ b/packages/core/src/core/registry/cards/issue.relations.view.yaml @@ -1,4 +1,4 @@ -capability_id: issue.relations.get +capability_id: issue.relations.view version: "1.0.0" description: Get issue parent/children/blocking relations. input_schema: diff --git a/packages/core/src/core/registry/cards/issue.triage.composite.yaml b/packages/core/src/core/registry/cards/issue.triage.composite.yaml deleted file mode 100644 index a3979235..00000000 --- a/packages/core/src/core/registry/cards/issue.triage.composite.yaml +++ /dev/null @@ -1,40 +0,0 @@ -capability_id: issue.triage.composite -version: "1.0.0" -description: "Set issue labels and create an issue comment in a single GraphQL batch call." -input_schema: - type: object - required: [issueId, labelIds, body] - properties: - issueId: { type: string, minLength: 1 } - labelIds: - type: array - minItems: 1 - items: { type: string, minLength: 1 } - body: { type: string, minLength: 1 } - additionalProperties: false -output_schema: - type: object - required: [issueId, commentId, labels, body, url] - properties: - issueId: { type: string, minLength: 1 } - commentId: { type: string, minLength: 1 } - labels: - type: array - items: { type: string, minLength: 1 } - body: { type: string } - url: { type: string } - additionalProperties: false -routing: - preferred: graphql - fallbacks: [] -composite: - steps: - - capability_id: issue.labels.update - params_map: - issueId: issueId - labels: labelIds - - capability_id: issue.comments.create - params_map: - issueId: issueId - body: body - output_strategy: merge diff --git a/packages/core/src/core/registry/cards/issue.update.composite.yaml b/packages/core/src/core/registry/cards/issue.update.composite.yaml deleted file mode 100644 index 6b6815fc..00000000 --- a/packages/core/src/core/registry/cards/issue.update.composite.yaml +++ /dev/null @@ -1,62 +0,0 @@ -capability_id: issue.update.composite -version: "1.0.0" -description: "Update issue fields, labels, assignees, and milestone in a single GraphQL batch call." -input_schema: - type: object - required: [issueId] - properties: - issueId: { type: string, minLength: 1 } - title: { type: string } - body: { type: string } - labelIds: - type: array - items: { type: string, minLength: 1 } - assigneeIds: - type: array - items: { type: string, minLength: 1 } - milestoneId: { type: [string, "null"] } - additionalProperties: false -output_schema: - type: object - required: [id, number, labels, assignees, milestoneNumber] - properties: - id: { type: string, minLength: 1 } - number: { type: integer, minimum: 1 } - title: { type: string } - state: { type: string } - url: { type: string, minLength: 1 } - labels: - type: array - items: { type: string, minLength: 1 } - assignees: - type: array - items: { type: string, minLength: 1 } - milestoneNumber: { type: [integer, "null"] } - additionalProperties: false -routing: - preferred: graphql - fallbacks: [] -composite: - steps: - - capability_id: issue.update - requires_any_of: [title, body] - params_map: - issueId: issueId - title: title - body: body - - capability_id: issue.labels.update - requires_any_of: [labelIds] - params_map: - issueId: issueId - labels: labelIds - - capability_id: issue.assignees.update - requires_any_of: [assigneeIds] - params_map: - issueId: issueId - assignees: assigneeIds - - capability_id: issue.milestone.set - requires_any_of: [milestoneId] - params_map: - issueId: issueId - milestoneNumber: milestoneId - output_strategy: merge diff --git a/packages/core/src/core/registry/cards/pr.assignees.update.yaml b/packages/core/src/core/registry/cards/pr.assignees.add.yaml similarity index 51% rename from packages/core/src/core/registry/cards/pr.assignees.update.yaml rename to packages/core/src/core/registry/cards/pr.assignees.add.yaml index 016b653c..f6105c3b 100644 --- a/packages/core/src/core/registry/cards/pr.assignees.update.yaml +++ b/packages/core/src/core/registry/cards/pr.assignees.add.yaml @@ -1,35 +1,26 @@ -capability_id: pr.assignees.update +capability_id: pr.assignees.add version: "1.0.0" -description: Update pull request assignees. +description: Add assignees to a pull request without replacing existing ones. input_schema: type: object - required: [owner, name, prNumber] + required: [owner, name, prNumber, assignees] properties: owner: { type: string, minLength: 1 } name: { type: string, minLength: 1 } prNumber: { type: integer, minimum: 1 } - add: + assignees: type: array items: { type: string, minLength: 1 } - remove: - type: array - items: { type: string, minLength: 1 } - anyOf: - - required: [add] - - required: [remove] + minItems: 1 additionalProperties: false output_schema: type: object - required: [prNumber, add, remove, updated] + required: [prNumber, added] properties: prNumber: { type: integer, minimum: 1 } - add: + added: type: array - items: { type: string, minLength: 1 } - remove: - type: array - items: { type: string, minLength: 1 } - updated: { type: boolean } + items: { type: string } additionalProperties: false routing: preferred: cli diff --git a/packages/core/src/core/registry/cards/pr.assignees.remove.yaml b/packages/core/src/core/registry/cards/pr.assignees.remove.yaml new file mode 100644 index 00000000..340aaab6 --- /dev/null +++ b/packages/core/src/core/registry/cards/pr.assignees.remove.yaml @@ -0,0 +1,29 @@ +capability_id: pr.assignees.remove +version: "1.0.0" +description: Remove specific assignees from a pull request. +input_schema: + type: object + required: [owner, name, prNumber, assignees] + properties: + owner: { type: string, minLength: 1 } + name: { type: string, minLength: 1 } + prNumber: { type: integer, minimum: 1 } + assignees: + type: array + items: { type: string, minLength: 1 } + minItems: 1 + additionalProperties: false +output_schema: + type: object + required: [prNumber, removed] + properties: + prNumber: { type: integer, minimum: 1 } + removed: + type: array + items: { type: string } + additionalProperties: false +routing: + preferred: cli + fallbacks: [] +cli: + command: pr edit diff --git a/packages/core/src/core/registry/cards/pr.checks.failed.yaml b/packages/core/src/core/registry/cards/pr.checks.failed.yaml deleted file mode 100644 index cbe9d094..00000000 --- a/packages/core/src/core/registry/cards/pr.checks.failed.yaml +++ /dev/null @@ -1,42 +0,0 @@ -capability_id: pr.checks.failed -version: "1.0.0" -description: List failed pull request checks. -input_schema: - type: object - required: [owner, name, prNumber] - properties: - owner: { type: string, minLength: 1 } - name: { type: string, minLength: 1 } - prNumber: { type: integer, minimum: 1 } - additionalProperties: false -output_schema: - type: object - required: [items, summary] - properties: - items: - type: array - items: - type: object - required: [name, state, workflow, link] - properties: - name: { type: [string, "null"] } - state: { type: [string, "null"] } - workflow: { type: [string, "null"] } - link: { type: [string, "null"] } - additionalProperties: false - summary: - type: object - required: [total, failed, pending, passed] - properties: - total: { type: integer, minimum: 0 } - failed: { type: integer, minimum: 0 } - pending: { type: integer, minimum: 0 } - passed: { type: integer, minimum: 0 } - additionalProperties: false - additionalProperties: false -routing: - preferred: cli - fallbacks: [] -cli: - command: pr checks - jsonFields: [name, state, workflow, link] diff --git a/packages/core/src/core/registry/cards/pr.checks.list.yaml b/packages/core/src/core/registry/cards/pr.checks.list.yaml index 6168d122..a4b4a9a7 100644 --- a/packages/core/src/core/registry/cards/pr.checks.list.yaml +++ b/packages/core/src/core/registry/cards/pr.checks.list.yaml @@ -8,6 +8,7 @@ input_schema: owner: { type: string, minLength: 1 } name: { type: string, minLength: 1 } prNumber: { type: integer, minimum: 1 } + state: { type: string, enum: [failed, pending, passed] } additionalProperties: false output_schema: type: object @@ -17,12 +18,24 @@ output_schema: type: array items: type: object - required: [name, state, workflow, link] + required: [name, state, workflow, link, annotations] properties: name: { type: [string, "null"] } state: { type: [string, "null"] } workflow: { type: [string, "null"] } link: { type: [string, "null"] } + annotations: + type: array + items: + type: object + required: [path, startLine, endLine, level, message] + properties: + path: { type: [string, "null"] } + startLine: { type: [integer, "null"] } + endLine: { type: [integer, "null"] } + level: { type: [string, "null"] } + message: { type: [string, "null"] } + additionalProperties: false additionalProperties: false summary: type: object diff --git a/packages/core/src/core/registry/cards/pr.checks.rerun_all.yaml b/packages/core/src/core/registry/cards/pr.checks.rerun.all.yaml similarity index 78% rename from packages/core/src/core/registry/cards/pr.checks.rerun_all.yaml rename to packages/core/src/core/registry/cards/pr.checks.rerun.all.yaml index c985704c..a70ff968 100644 --- a/packages/core/src/core/registry/cards/pr.checks.rerun_all.yaml +++ b/packages/core/src/core/registry/cards/pr.checks.rerun.all.yaml @@ -1,4 +1,4 @@ -capability_id: pr.checks.rerun_all +capability_id: pr.checks.rerun.all version: "1.0.0" description: Rerun all pull request workflow checks for a selected run. input_schema: @@ -12,11 +12,9 @@ input_schema: additionalProperties: false output_schema: type: object - required: [prNumber, runId, mode, queued] + required: [runId, queued] properties: - prNumber: { type: integer, minimum: 1 } runId: { type: integer, minimum: 1 } - mode: { type: string, const: all } queued: { type: boolean } additionalProperties: false routing: diff --git a/packages/core/src/core/registry/cards/pr.checks.rerun_failed.yaml b/packages/core/src/core/registry/cards/pr.checks.rerun.failed.yaml similarity index 72% rename from packages/core/src/core/registry/cards/pr.checks.rerun_failed.yaml rename to packages/core/src/core/registry/cards/pr.checks.rerun.failed.yaml index b0524857..5d793201 100644 --- a/packages/core/src/core/registry/cards/pr.checks.rerun_failed.yaml +++ b/packages/core/src/core/registry/cards/pr.checks.rerun.failed.yaml @@ -1,4 +1,4 @@ -capability_id: pr.checks.rerun_failed +capability_id: pr.checks.rerun.failed version: "1.0.0" description: Rerun failed pull request workflow checks for a selected run. input_schema: @@ -12,11 +12,9 @@ input_schema: additionalProperties: false output_schema: type: object - required: [prNumber, runId, mode, queued] + required: [runId, queued] properties: - prNumber: { type: integer, minimum: 1 } - runId: { type: integer, minimum: 1 } - mode: { type: string, const: failed } + runId: { type: integer } queued: { type: boolean } additionalProperties: false routing: diff --git a/packages/core/src/core/registry/cards/pr.review.list.yaml b/packages/core/src/core/registry/cards/pr.reviews.list.yaml similarity index 97% rename from packages/core/src/core/registry/cards/pr.review.list.yaml rename to packages/core/src/core/registry/cards/pr.reviews.list.yaml index b1cafd84..88a26f3f 100644 --- a/packages/core/src/core/registry/cards/pr.review.list.yaml +++ b/packages/core/src/core/registry/cards/pr.reviews.list.yaml @@ -1,4 +1,4 @@ -capability_id: pr.review.list +capability_id: pr.reviews.list version: "1.0.0" description: List pull request reviews. input_schema: diff --git a/packages/core/src/core/registry/cards/pr.review.request.yaml b/packages/core/src/core/registry/cards/pr.reviews.request.yaml similarity index 95% rename from packages/core/src/core/registry/cards/pr.review.request.yaml rename to packages/core/src/core/registry/cards/pr.reviews.request.yaml index 2863a5fa..d7da06ac 100644 --- a/packages/core/src/core/registry/cards/pr.review.request.yaml +++ b/packages/core/src/core/registry/cards/pr.reviews.request.yaml @@ -1,4 +1,4 @@ -capability_id: pr.review.request +capability_id: pr.reviews.request version: "1.0.0" description: Request pull request reviewers. input_schema: diff --git a/packages/core/src/core/registry/cards/pr.review.submit.yaml b/packages/core/src/core/registry/cards/pr.reviews.submit.yaml similarity index 97% rename from packages/core/src/core/registry/cards/pr.review.submit.yaml rename to packages/core/src/core/registry/cards/pr.reviews.submit.yaml index 00c5af78..323bfd5b 100644 --- a/packages/core/src/core/registry/cards/pr.review.submit.yaml +++ b/packages/core/src/core/registry/cards/pr.reviews.submit.yaml @@ -1,4 +1,4 @@ -capability_id: pr.review.submit +capability_id: pr.reviews.submit version: "1.0.0" description: Submit a pull request review (approve, request changes, or comment). GitHub requires a non-empty body for COMMENT and REQUEST_CHANGES events. input_schema: diff --git a/packages/core/src/core/registry/cards/pr.threads.composite.yaml b/packages/core/src/core/registry/cards/pr.threads.composite.yaml deleted file mode 100644 index 6627e5af..00000000 --- a/packages/core/src/core/registry/cards/pr.threads.composite.yaml +++ /dev/null @@ -1,63 +0,0 @@ -capability_id: pr.threads.composite -version: "1.0.0" -description: "Reply to, resolve, unresolve, or reply-and-resolve multiple PR review threads in a single batched call. Supports mixed actions per thread." -input_schema: - type: object - required: [threads] - properties: - threads: - type: array - minItems: 1 - items: - type: object - required: [threadId, action] - properties: - threadId: - type: string - minLength: 1 - action: - type: string - enum: [reply, resolve, reply_and_resolve, unresolve] - body: - type: string - minLength: 1 - additionalProperties: false - additionalProperties: false -output_schema: - type: object - required: [results] - properties: - results: - type: array - items: - type: object - required: [id] - properties: - id: - type: string - isResolved: - type: boolean - additionalProperties: false - additionalProperties: false -routing: - preferred: graphql - fallbacks: [] -composite: - steps: - - capability_id: pr.thread.reply - foreach: threads - actions: [reply, reply_and_resolve] - params_map: - threadId: threadId - body: body - - capability_id: pr.thread.resolve - foreach: threads - actions: [resolve, reply_and_resolve] - params_map: - threadId: threadId - - capability_id: pr.thread.unresolve - foreach: threads - actions: [unresolve] - params_map: - threadId: threadId - output_strategy: array diff --git a/packages/core/src/core/registry/cards/pr.thread.list.yaml b/packages/core/src/core/registry/cards/pr.threads.list.yaml similarity index 99% rename from packages/core/src/core/registry/cards/pr.thread.list.yaml rename to packages/core/src/core/registry/cards/pr.threads.list.yaml index 543bc3ad..d908d6c6 100644 --- a/packages/core/src/core/registry/cards/pr.thread.list.yaml +++ b/packages/core/src/core/registry/cards/pr.threads.list.yaml @@ -1,4 +1,4 @@ -capability_id: pr.thread.list +capability_id: pr.threads.list version: "1.0.0" description: List pull request review threads (unresolved only by default; pass unresolvedOnly:false to include resolved). input_schema: diff --git a/packages/core/src/core/registry/cards/pr.thread.reply.yaml b/packages/core/src/core/registry/cards/pr.threads.reply.yaml similarity index 78% rename from packages/core/src/core/registry/cards/pr.thread.reply.yaml rename to packages/core/src/core/registry/cards/pr.threads.reply.yaml index e03c426b..df9a3634 100644 --- a/packages/core/src/core/registry/cards/pr.thread.reply.yaml +++ b/packages/core/src/core/registry/cards/pr.threads.reply.yaml @@ -1,4 +1,4 @@ -capability_id: pr.thread.reply +capability_id: pr.threads.reply version: "1.0.0" description: Reply to a pull request review thread. input_schema: @@ -10,10 +10,12 @@ input_schema: additionalProperties: false output_schema: type: object - required: [id, isResolved] + required: [id, isResolved, commentId, commentUrl] properties: id: { type: string, minLength: 1 } isResolved: { type: boolean } + commentId: { type: string } + commentUrl: { type: string } additionalProperties: false routing: preferred: graphql diff --git a/packages/core/src/core/registry/cards/pr.thread.resolve.yaml b/packages/core/src/core/registry/cards/pr.threads.resolve.yaml similarity index 94% rename from packages/core/src/core/registry/cards/pr.thread.resolve.yaml rename to packages/core/src/core/registry/cards/pr.threads.resolve.yaml index e1c70a8f..5f3dd804 100644 --- a/packages/core/src/core/registry/cards/pr.thread.resolve.yaml +++ b/packages/core/src/core/registry/cards/pr.threads.resolve.yaml @@ -1,4 +1,4 @@ -capability_id: pr.thread.resolve +capability_id: pr.threads.resolve version: "1.0.0" description: Resolve a pull request review thread. input_schema: diff --git a/packages/core/src/core/registry/cards/pr.thread.unresolve.yaml b/packages/core/src/core/registry/cards/pr.threads.unresolve.yaml similarity index 93% rename from packages/core/src/core/registry/cards/pr.thread.unresolve.yaml rename to packages/core/src/core/registry/cards/pr.threads.unresolve.yaml index 80d361ab..8af6201f 100644 --- a/packages/core/src/core/registry/cards/pr.thread.unresolve.yaml +++ b/packages/core/src/core/registry/cards/pr.threads.unresolve.yaml @@ -1,4 +1,4 @@ -capability_id: pr.thread.unresolve +capability_id: pr.threads.unresolve version: "1.0.0" description: Unresolve a pull request review thread. input_schema: diff --git a/packages/core/src/core/registry/cards/project_v2.item.field.update.yaml b/packages/core/src/core/registry/cards/project_v2.items.field.update.yaml similarity index 94% rename from packages/core/src/core/registry/cards/project_v2.item.field.update.yaml rename to packages/core/src/core/registry/cards/project_v2.items.field.update.yaml index babad20d..6087997a 100644 --- a/packages/core/src/core/registry/cards/project_v2.item.field.update.yaml +++ b/packages/core/src/core/registry/cards/project_v2.items.field.update.yaml @@ -1,4 +1,4 @@ -capability_id: project_v2.item.field.update +capability_id: project_v2.items.field.update version: "1.0.0" description: Update a field on a Projects v2 project item. input_schema: diff --git a/packages/core/src/core/registry/cards/project_v2.item.add_issue.yaml b/packages/core/src/core/registry/cards/project_v2.items.issue.add.yaml similarity index 93% rename from packages/core/src/core/registry/cards/project_v2.item.add_issue.yaml rename to packages/core/src/core/registry/cards/project_v2.items.issue.add.yaml index e5d58caa..66a4dffe 100644 --- a/packages/core/src/core/registry/cards/project_v2.item.add_issue.yaml +++ b/packages/core/src/core/registry/cards/project_v2.items.issue.add.yaml @@ -1,4 +1,4 @@ -capability_id: project_v2.item.add_issue +capability_id: project_v2.items.issue.add version: "1.0.0" description: Add an issue to a Projects v2 project. input_schema: diff --git a/packages/core/src/core/registry/cards/project_v2.items.issue.remove.yaml b/packages/core/src/core/registry/cards/project_v2.items.issue.remove.yaml new file mode 100644 index 00000000..0145f080 --- /dev/null +++ b/packages/core/src/core/registry/cards/project_v2.items.issue.remove.yaml @@ -0,0 +1,23 @@ +capability_id: project_v2.items.issue.remove +version: "1.0.0" +description: Remove an issue from a Projects v2 project. +input_schema: + type: object + required: [owner, projectNumber, itemId] + properties: + owner: { type: string, minLength: 1 } + projectNumber: { type: integer, minimum: 1 } + itemId: { type: string, minLength: 1 } + additionalProperties: false +output_schema: + type: object + required: [itemId, removed] + properties: + itemId: { type: string, minLength: 1 } + removed: { type: boolean } + additionalProperties: false +routing: + preferred: cli + fallbacks: [] +cli: + command: project item-delete diff --git a/packages/core/src/core/registry/cards/project_v2.org.get.yaml b/packages/core/src/core/registry/cards/project_v2.org.view.yaml similarity index 95% rename from packages/core/src/core/registry/cards/project_v2.org.get.yaml rename to packages/core/src/core/registry/cards/project_v2.org.view.yaml index d1cd6b76..42f175d6 100644 --- a/packages/core/src/core/registry/cards/project_v2.org.get.yaml +++ b/packages/core/src/core/registry/cards/project_v2.org.view.yaml @@ -1,4 +1,4 @@ -capability_id: project_v2.org.get +capability_id: project_v2.org.view version: "1.0.0" description: Get an organization Projects v2 project. input_schema: diff --git a/packages/core/src/core/registry/cards/project_v2.user.get.yaml b/packages/core/src/core/registry/cards/project_v2.user.view.yaml similarity index 95% rename from packages/core/src/core/registry/cards/project_v2.user.get.yaml rename to packages/core/src/core/registry/cards/project_v2.user.view.yaml index 12046e88..d767e63a 100644 --- a/packages/core/src/core/registry/cards/project_v2.user.get.yaml +++ b/packages/core/src/core/registry/cards/project_v2.user.view.yaml @@ -1,4 +1,4 @@ -capability_id: project_v2.user.get +capability_id: project_v2.user.view version: "1.0.0" description: Get a user Projects v2 project. input_schema: diff --git a/packages/core/src/core/registry/cards/release.create_draft.yaml b/packages/core/src/core/registry/cards/release.create.yaml similarity index 96% rename from packages/core/src/core/registry/cards/release.create_draft.yaml rename to packages/core/src/core/registry/cards/release.create.yaml index 9415f4c9..d0953fd9 100644 --- a/packages/core/src/core/registry/cards/release.create_draft.yaml +++ b/packages/core/src/core/registry/cards/release.create.yaml @@ -1,4 +1,4 @@ -capability_id: release.create_draft +capability_id: release.create version: "1.0.0" description: Create a draft release. input_schema: diff --git a/packages/core/src/core/registry/cards/release.publish_draft.yaml b/packages/core/src/core/registry/cards/release.publish.yaml similarity index 96% rename from packages/core/src/core/registry/cards/release.publish_draft.yaml rename to packages/core/src/core/registry/cards/release.publish.yaml index 81b9b349..f79626b8 100644 --- a/packages/core/src/core/registry/cards/release.publish_draft.yaml +++ b/packages/core/src/core/registry/cards/release.publish.yaml @@ -1,4 +1,4 @@ -capability_id: release.publish_draft +capability_id: release.publish version: "1.0.0" description: Publish an existing draft release. input_schema: diff --git a/packages/core/src/core/registry/cards/release.get.yaml b/packages/core/src/core/registry/cards/release.view.yaml similarity index 96% rename from packages/core/src/core/registry/cards/release.get.yaml rename to packages/core/src/core/registry/cards/release.view.yaml index 519879c0..c2253a7f 100644 --- a/packages/core/src/core/registry/cards/release.get.yaml +++ b/packages/core/src/core/registry/cards/release.view.yaml @@ -1,4 +1,4 @@ -capability_id: release.get +capability_id: release.view version: "1.0.0" description: Get release details by tag name. input_schema: diff --git a/packages/core/src/core/registry/cards/workflow.dispatch.run.yaml b/packages/core/src/core/registry/cards/workflow.dispatch.yaml similarity index 95% rename from packages/core/src/core/registry/cards/workflow.dispatch.run.yaml rename to packages/core/src/core/registry/cards/workflow.dispatch.yaml index df5a7ebb..dc05defb 100644 --- a/packages/core/src/core/registry/cards/workflow.dispatch.run.yaml +++ b/packages/core/src/core/registry/cards/workflow.dispatch.yaml @@ -1,4 +1,4 @@ -capability_id: workflow.dispatch.run +capability_id: workflow.dispatch version: "1.0.0" description: Trigger a workflow dispatch event. input_schema: diff --git a/packages/core/src/core/registry/cards/workflow.job.logs.get.yaml b/packages/core/src/core/registry/cards/workflow.job.logs.view.yaml similarity index 95% rename from packages/core/src/core/registry/cards/workflow.job.logs.get.yaml rename to packages/core/src/core/registry/cards/workflow.job.logs.view.yaml index e9835551..ee3bca8e 100644 --- a/packages/core/src/core/registry/cards/workflow.job.logs.get.yaml +++ b/packages/core/src/core/registry/cards/workflow.job.logs.view.yaml @@ -1,4 +1,4 @@ -capability_id: workflow.job.logs.get +capability_id: workflow.job.logs.view version: "1.0.0" description: Fetch and analyze workflow job logs. input_schema: diff --git a/packages/core/src/core/registry/cards/workflow.run.rerun_all.yaml b/packages/core/src/core/registry/cards/workflow.run.rerun.all.yaml similarity index 82% rename from packages/core/src/core/registry/cards/workflow.run.rerun_all.yaml rename to packages/core/src/core/registry/cards/workflow.run.rerun.all.yaml index c8807249..50f6012d 100644 --- a/packages/core/src/core/registry/cards/workflow.run.rerun_all.yaml +++ b/packages/core/src/core/registry/cards/workflow.run.rerun.all.yaml @@ -1,4 +1,4 @@ -capability_id: workflow.run.rerun_all +capability_id: workflow.run.rerun.all version: "1.0.0" description: Rerun all jobs in a workflow run. input_schema: @@ -11,10 +11,10 @@ input_schema: additionalProperties: false output_schema: type: object - required: [runId, status] + required: [runId, queued] properties: runId: { type: integer, minimum: 1 } - status: { const: requested } + queued: { type: boolean } additionalProperties: false routing: preferred: cli diff --git a/packages/core/src/core/registry/cards/workflow.run.rerun_failed.yaml b/packages/core/src/core/registry/cards/workflow.run.rerun.failed.yaml similarity index 81% rename from packages/core/src/core/registry/cards/workflow.run.rerun_failed.yaml rename to packages/core/src/core/registry/cards/workflow.run.rerun.failed.yaml index 68d565e6..0a4d3446 100644 --- a/packages/core/src/core/registry/cards/workflow.run.rerun_failed.yaml +++ b/packages/core/src/core/registry/cards/workflow.run.rerun.failed.yaml @@ -1,4 +1,4 @@ -capability_id: workflow.run.rerun_failed +capability_id: workflow.run.rerun.failed version: "1.0.0" description: Rerun failed jobs for a workflow run. input_schema: @@ -11,10 +11,10 @@ input_schema: additionalProperties: false output_schema: type: object - required: [runId, rerunFailed] + required: [runId, queued] properties: runId: { type: integer, minimum: 1 } - rerunFailed: { type: boolean } + queued: { type: boolean } additionalProperties: false routing: preferred: cli diff --git a/packages/core/src/core/registry/cards/workflow.get.yaml b/packages/core/src/core/registry/cards/workflow.view.yaml similarity index 91% rename from packages/core/src/core/registry/cards/workflow.get.yaml rename to packages/core/src/core/registry/cards/workflow.view.yaml index dd3d8a4c..2c828f1a 100644 --- a/packages/core/src/core/registry/cards/workflow.get.yaml +++ b/packages/core/src/core/registry/cards/workflow.view.yaml @@ -1,6 +1,6 @@ -capability_id: workflow.get +capability_id: workflow.view version: "1.0.0" -description: Get one repository workflow. +description: View one repository workflow. input_schema: type: object required: [owner, name, workflowId] diff --git a/packages/core/src/core/registry/index.ts b/packages/core/src/core/registry/index.ts index e259acb9..dc2064f8 100644 --- a/packages/core/src/core/registry/index.ts +++ b/packages/core/src/core/registry/index.ts @@ -20,8 +20,6 @@ function loadCardsFromYaml(): OperationCard[] { "repo.view", "repo.labels.list", "repo.issue_types.list", - "issue.triage.composite", - "issue.update.composite", "issue.view", "issue.list", "issue.comments.list", @@ -30,60 +28,63 @@ function loadCardsFromYaml(): OperationCard[] { "issue.close", "issue.reopen", "issue.delete", - "issue.labels.update", + "issue.labels.set", "issue.labels.add", - "issue.assignees.update", + "issue.labels.remove", + "issue.assignees.set", + "issue.assignees.add", + "issue.assignees.remove", "issue.milestone.set", + "issue.milestone.clear", "issue.comments.create", - "issue.linked_prs.list", - "issue.relations.get", - "issue.parent.set", - "issue.parent.remove", - "issue.blocked_by.add", - "issue.blocked_by.remove", - "pr.threads.composite", + "issue.relations.prs.list", + "issue.relations.view", + "issue.relations.parent.set", + "issue.relations.parent.remove", + "issue.relations.blocked_by.add", + "issue.relations.blocked_by.remove", "pr.view", "pr.list", "pr.create", "pr.update", - "pr.thread.list", - "pr.thread.reply", - "pr.thread.resolve", - "pr.thread.unresolve", - "pr.review.list", - "pr.review.request", - "pr.review.submit", + "pr.threads.list", + "pr.threads.reply", + "pr.threads.resolve", + "pr.threads.unresolve", + "pr.reviews.list", + "pr.reviews.request", + "pr.reviews.submit", "pr.diff.files", "pr.diff.view", "pr.checks.list", - "pr.checks.failed", - "pr.checks.rerun_failed", - "pr.checks.rerun_all", + "pr.checks.rerun.failed", + "pr.checks.rerun.all", "pr.merge.status", "pr.merge", - "pr.assignees.update", + "pr.assignees.add", + "pr.assignees.remove", "pr.branch.update", - "check_run.annotations.list", "workflow.list", - "workflow.get", - "project_v2.org.get", - "project_v2.user.get", + "workflow.view", + "project_v2.org.view", + "project_v2.user.view", "project_v2.fields.list", "project_v2.items.list", - "project_v2.item.add_issue", - "project_v2.item.field.update", + "project_v2.items.issue.add", + "project_v2.items.issue.remove", + "project_v2.items.field.update", "release.list", - "release.get", - "release.create_draft", + "release.view", + "release.create", "release.update", - "release.publish_draft", - "workflow.dispatch.run", - "workflow.job.logs.get", + "release.publish", + "workflow.dispatch", + "workflow.job.logs.view", "workflow.job.logs.raw", "workflow.run.artifacts.list", "workflow.run.cancel", - "workflow.run.rerun_all", - "workflow.run.rerun_failed", + "workflow.run.rerun.all", + "workflow.run.rerun.failed", "workflow.run.view", "workflow.runs.list", ] diff --git a/packages/core/src/core/registry/operation-card-schema.ts b/packages/core/src/core/registry/operation-card-schema.ts index f11218ff..d058e098 100644 --- a/packages/core/src/core/registry/operation-card-schema.ts +++ b/packages/core/src/core/registry/operation-card-schema.ts @@ -93,38 +93,6 @@ export const operationCardSchema = { }, additionalProperties: false, }, - composite: { - type: "object", - required: ["steps", "output_strategy"], - properties: { - steps: { - type: "array", - minItems: 1, - items: { - type: "object", - required: ["capability_id", "params_map"], - properties: { - capability_id: { type: "string", minLength: 1 }, - foreach: { type: "string", minLength: 1 }, - actions: { - type: "array", - minItems: 1, - items: { type: "string", minLength: 1 }, - }, - requires_any_of: { - type: "array", - minItems: 1, - items: { type: "string", minLength: 1 }, - }, - params_map: { type: "object" }, - }, - additionalProperties: false, - }, - }, - output_strategy: { enum: ["merge", "array", "last"] }, - }, - additionalProperties: false, - }, examples: { type: "array", items: { diff --git a/packages/core/src/core/registry/types.ts b/packages/core/src/core/registry/types.ts index afa46a9c..335a58b3 100644 --- a/packages/core/src/core/registry/types.ts +++ b/packages/core/src/core/registry/types.ts @@ -8,19 +8,6 @@ export interface SuitabilityRule { reason: string } -export interface CompositeStep { - capability_id: string - foreach?: string - actions?: string[] - requires_any_of?: string[] - params_map: Record -} - -export interface CompositeConfig { - steps: CompositeStep[] - output_strategy: "merge" | "array" | "last" -} - export interface OperationCard> { capability_id: string version: string @@ -48,10 +35,6 @@ export interface OperationCard> { rest?: { endpoints: Array<{ method: string; path: string }> } - composite?: { - steps: CompositeStep[] - output_strategy: "merge" | "array" | "last" - } examples?: Array<{ title: string input: Input diff --git a/packages/core/src/core/routing/engine.ts b/packages/core/src/core/routing/engine.ts index 74dc14a7..54417661 100644 --- a/packages/core/src/core/routing/engine.ts +++ b/packages/core/src/core/routing/engine.ts @@ -1,8 +1,6 @@ import type { ResultEnvelope, RouteSource } from "@core/core/contracts/envelope.js" import type { TaskRequest } from "@core/core/contracts/task.js" import { errorCodes } from "@core/core/errors/codes.js" -import { mapErrorToCode } from "@core/core/errors/map-error.js" -import { expandCompositeSteps } from "@core/core/execute/composite.js" import { execute } from "@core/core/execute/execute.js" import { type CliCapabilityId, @@ -16,7 +14,6 @@ import { preflightCheck } from "@core/core/execution/preflight.js" import { getOperationCard } from "@core/core/registry/index.js" import { routePreferenceOrder } from "@core/core/routing/policy.js" import type { RouteReasonCode } from "@core/core/routing/reason-codes.js" -import { buildBatchMutation } from "@core/gql/batch.js" import type { GithubClient } from "@core/gql/github-client.js" type ExecutionDeps = { @@ -97,92 +94,6 @@ async function detectCliEnvironmentCached(runner: CliCommandRunner): Promise>, - input: Record, - deps: ExecutionDeps, - reason: RouteReasonCode, -): Promise { - if (!card.composite) { - return normalizeError( - { - code: errorCodes.Validation, - message: "Card does not have composite config", - retryable: false, - }, - "graphql", - { capabilityId: card.capability_id, reason }, - ) - } - - try { - const expandedOperations = expandCompositeSteps(card.composite, input) - - if (expandedOperations.length === 0) { - return normalizeError( - { - code: errorCodes.Validation, - message: "No operations to execute", - retryable: false, - }, - "graphql", - { capabilityId: card.capability_id, reason }, - ) - } - - const batchInput = expandedOperations.map((op) => ({ - alias: op.alias, - mutation: op.mutation, - variables: op.variables, - })) - const { document, variables } = buildBatchMutation(batchInput) - const batchResult = await deps.githubClient.query(document, variables) - - const results: unknown[] = [] - const resultsByAlias = batchResult as Record - for (const op of expandedOperations) { - const aliasedResult = resultsByAlias[op.alias] - if (aliasedResult === undefined) { - throw new Error(`Missing result for alias "${op.alias}" in batch response`) - } - const mapped = op.mapResponse(aliasedResult) - results.push(mapped) - } - - let aggregatedData: unknown - if (card.composite.output_strategy === "array") { - aggregatedData = { results } - } else if (card.composite.output_strategy === "merge") { - aggregatedData = Object.assign({}, ...results) - } else { - aggregatedData = results[results.length - 1] - } - - return { - ok: true, - data: aggregatedData, - meta: { - capability_id: card.capability_id, - route_used: "graphql", - reason, - }, - } - } catch (error) { - const code = mapErrorToCode(error) - const message = error instanceof Error ? error.message : String(error) || "Unknown error" - - return normalizeError( - { - code, - message, - retryable: code !== errorCodes.Validation, - }, - "graphql", - { capabilityId: card.capability_id, reason }, - ) - } -} - export async function executeTask( request: TaskRequest, deps: ExecutionDeps, @@ -259,10 +170,6 @@ export async function executeTask( }, routes: { graphql: async () => { - if (card.composite) { - return executeComposite(card, request.input as Record, deps, reason) - } - return runGraphqlCapability( deps.githubClient, request.task, diff --git a/packages/core/src/gql/assertions.ts b/packages/core/src/gql/assertions.ts index e2b00f9d..af77750f 100644 --- a/packages/core/src/gql/assertions.ts +++ b/packages/core/src/gql/assertions.ts @@ -135,11 +135,8 @@ export function assertIssueAssigneesUpdateInput(input: IssueAssigneesUpdateInput export function assertIssueMilestoneSetInput(input: IssueMilestoneSetInput): void { assertIssueMutationInput({ issueId: input.issueId }) - if ( - input.milestoneNumber !== null && - (!Number.isInteger(input.milestoneNumber) || input.milestoneNumber <= 0) - ) { - throw new Error("Milestone number must be a positive integer or null") + if (!Number.isInteger(input.milestoneNumber) || input.milestoneNumber <= 0) { + throw new Error("Milestone number must be a positive integer") } } diff --git a/packages/core/src/gql/builders.ts b/packages/core/src/gql/builders.ts index af4816e9..12128846 100644 --- a/packages/core/src/gql/builders.ts +++ b/packages/core/src/gql/builders.ts @@ -67,7 +67,12 @@ export const replyBuilder: OperationBuilder = { throw new Error("Review thread mutation failed") } - return { id: comment.id } + return { + id: comment.id, + isResolved: false, + commentId: comment.id, + commentUrl: typeof comment.url === "string" ? comment.url : "", + } }, } @@ -269,12 +274,12 @@ const issueCommentCreateBuilder: OperationBuilder = { } export const OPERATION_BUILDERS: Record = { - "pr.thread.reply": replyBuilder, - "pr.thread.resolve": resolveBuilder, - "pr.thread.unresolve": unresolveBuilder, + "pr.threads.reply": replyBuilder, + "pr.threads.resolve": resolveBuilder, + "pr.threads.unresolve": unresolveBuilder, "issue.update": issueUpdateBuilder, - "issue.labels.update": issueLabelsUpdateBuilder, - "issue.assignees.update": issueAssigneesUpdateBuilder, + "issue.labels.set": issueLabelsUpdateBuilder, + "issue.assignees.set": issueAssigneesUpdateBuilder, "issue.milestone.set": issueMilestoneSetBuilder, "issue.comments.create": issueCommentCreateBuilder, } diff --git a/packages/core/src/gql/capability-registry.ts b/packages/core/src/gql/capability-registry.ts index 4995fa65..a2488a62 100644 --- a/packages/core/src/gql/capability-registry.ts +++ b/packages/core/src/gql/capability-registry.ts @@ -117,7 +117,7 @@ const handlers = new Map([ }, ], [ - "issue.labels.update", + "issue.labels.set", (c, p) => { if (!c.updateIssueLabels) { throw new Error("updateIssueLabels operation not available") @@ -126,7 +126,7 @@ const handlers = new Map([ }, ], [ - "issue.assignees.update", + "issue.assignees.set", (c, p) => { if (!c.updateIssueAssignees) { throw new Error("updateIssueAssignees operation not available") @@ -153,7 +153,7 @@ const handlers = new Map([ }, ], [ - "issue.linked_prs.list", + "issue.relations.prs.list", (c, p) => { if (!c.fetchIssueLinkedPrs) { throw new Error("fetchIssueLinkedPrs operation not available") @@ -162,7 +162,7 @@ const handlers = new Map([ }, ], [ - "issue.relations.get", + "issue.relations.view", (c, p) => { if (!c.fetchIssueRelations) { throw new Error("fetchIssueRelations operation not available") @@ -171,7 +171,7 @@ const handlers = new Map([ }, ], [ - "issue.parent.set", + "issue.relations.parent.set", (c, p) => { if (!c.setIssueParent) { throw new Error("setIssueParent operation not available") @@ -180,7 +180,7 @@ const handlers = new Map([ }, ], [ - "issue.parent.remove", + "issue.relations.parent.remove", (c, p) => { if (!c.removeIssueParent) { throw new Error("removeIssueParent operation not available") @@ -189,7 +189,7 @@ const handlers = new Map([ }, ], [ - "issue.blocked_by.add", + "issue.relations.blocked_by.add", (c, p) => { if (!c.addIssueBlockedBy) { throw new Error("addIssueBlockedBy operation not available") @@ -198,7 +198,7 @@ const handlers = new Map([ }, ], [ - "issue.blocked_by.remove", + "issue.relations.blocked_by.remove", (c, p) => { if (!c.removeIssueBlockedBy) { throw new Error("removeIssueBlockedBy operation not available") @@ -210,36 +210,36 @@ const handlers = new Map([ // PR queries ["pr.view", (c, p) => c.fetchPrView(p as PrViewInput)], ["pr.list", (c, p) => c.fetchPrList(withDefaultFirst(p) as PrListInput)], - ["pr.review.list", (c, p) => c.fetchPrReviewsList(withDefaultFirst(p) as PrReviewsListInput)], + ["pr.reviews.list", (c, p) => c.fetchPrReviewsList(withDefaultFirst(p) as PrReviewsListInput)], ["pr.diff.files", (c, p) => c.fetchPrDiffListFiles(withDefaultFirst(p) as PrDiffListFilesInput)], ["pr.merge.status", (c, p) => c.fetchPrMergeStatus(p as PrMergeStatusInput)], - ["pr.thread.list", (c, p) => c.fetchPrCommentsList(withDefaultFirst(p) as PrCommentsListInput)], + ["pr.threads.list", (c, p) => c.fetchPrCommentsList(withDefaultFirst(p) as PrCommentsListInput)], // PR mutations [ - "pr.thread.reply", + "pr.threads.reply", (c, p) => { - const threadId = requireNonEmptyString(p, "threadId", "pr.thread.reply") - const body = requireNonEmptyString(p, "body", "pr.thread.reply") + const threadId = requireNonEmptyString(p, "threadId", "pr.threads.reply") + const body = requireNonEmptyString(p, "body", "pr.threads.reply") return c.replyToReviewThread({ threadId, body }) }, ], [ - "pr.thread.resolve", + "pr.threads.resolve", (c, p) => { - const threadId = requireNonEmptyString(p, "threadId", "pr.thread.resolve") + const threadId = requireNonEmptyString(p, "threadId", "pr.threads.resolve") return c.resolveReviewThread({ threadId }) }, ], [ - "pr.thread.unresolve", + "pr.threads.unresolve", (c, p) => { - const threadId = requireNonEmptyString(p, "threadId", "pr.thread.unresolve") + const threadId = requireNonEmptyString(p, "threadId", "pr.threads.unresolve") return c.unresolveReviewThread({ threadId }) }, ], [ - "pr.review.submit", + "pr.reviews.submit", (c, p) => { if (!c.submitPrReview) { throw new Error("submitPrReview operation not available") diff --git a/packages/core/src/gql/domains/issue-mutations.ts b/packages/core/src/gql/domains/issue-mutations.ts index c693c60c..4640e670 100644 --- a/packages/core/src/gql/domains/issue-mutations.ts +++ b/packages/core/src/gql/domains/issue-mutations.ts @@ -354,21 +354,18 @@ export async function runIssueMilestoneSet( assertIssueMilestoneSetInput(input) const client = createGraphqlRequestClient(transport) - let milestoneId: string | null = null - if (input.milestoneNumber !== null) { - const lookupResult = await getIssueMilestoneLookupSdk(client).IssueMilestoneLookup({ - issueId: input.issueId, - milestoneNumber: input.milestoneNumber, - }) - - const resolvedId = asRecord( - asRecord(asRecord(asRecord(lookupResult)?.node)?.repository)?.milestone, - )?.id - if (typeof resolvedId !== "string" || resolvedId.length === 0) { - throw new Error(`Milestone not found: ${input.milestoneNumber}`) - } - milestoneId = resolvedId + const lookupResult = await getIssueMilestoneLookupSdk(client).IssueMilestoneLookup({ + issueId: input.issueId, + milestoneNumber: input.milestoneNumber, + }) + + const resolvedId = asRecord( + asRecord(asRecord(asRecord(lookupResult)?.node)?.repository)?.milestone, + )?.id + if (typeof resolvedId !== "string" || resolvedId.length === 0) { + throw new Error(`Milestone not found: ${input.milestoneNumber}`) } + const milestoneId = resolvedId const result = await getIssueMilestoneSetSdk(client).IssueMilestoneSet({ issueId: input.issueId, @@ -538,6 +535,7 @@ export async function runIssueParentSet( return { issueId: subIssue.id, parentIssueId: parentIssue.id, + updated: true, } } @@ -598,6 +596,7 @@ export async function runIssueBlockedByAdd( return { issueId: issue.id, blockedByIssueId: blockingIssue.id, + added: true, } } diff --git a/packages/core/src/gql/domains/pr-mutations.ts b/packages/core/src/gql/domains/pr-mutations.ts index d8a82e96..52a0ae48 100644 --- a/packages/core/src/gql/domains/pr-mutations.ts +++ b/packages/core/src/gql/domains/pr-mutations.ts @@ -24,6 +24,7 @@ import type { PrReviewSubmitInput, PrReviewThreadCommentData, PrReviewThreadData, + ReplyToReviewThreadData, ReplyToReviewThreadInput, ReviewThreadMutationData, ReviewThreadMutationInput, @@ -209,7 +210,7 @@ function parseReviewThreadMutationResult( export async function runReplyToReviewThread( transport: GraphqlTransport, input: ReplyToReviewThreadInput, -): Promise { +): Promise { assertReplyToReviewThreadInput(input) const client = createGraphqlRequestClient(transport) @@ -234,6 +235,8 @@ export async function runReplyToReviewThread( return { id: input.threadId, isResolved: Boolean(threadNode.isResolved), + commentId: comment.id, + commentUrl: typeof comment.url === "string" ? comment.url : "", } } diff --git a/packages/core/src/gql/types.ts b/packages/core/src/gql/types.ts index 0f91ce5c..30408599 100644 --- a/packages/core/src/gql/types.ts +++ b/packages/core/src/gql/types.ts @@ -61,7 +61,7 @@ export type IssueAssigneesUpdateInput = { export type IssueMilestoneSetInput = { issueId: string - milestoneNumber: number | null + milestoneNumber: number } export type IssueCommentCreateInput = { @@ -208,6 +208,7 @@ export type IssueRelationsGetData = { export type IssueParentSetData = { issueId: string parentIssueId: string + updated: boolean } export type IssueParentRemoveData = { @@ -218,6 +219,7 @@ export type IssueParentRemoveData = { export type IssueBlockedByData = { issueId: string blockedByIssueId: string + added?: boolean removed?: boolean } @@ -347,6 +349,11 @@ export type ReviewThreadMutationData = { isResolved: boolean } +export type ReplyToReviewThreadData = ReviewThreadMutationData & { + commentId: string + commentUrl: string +} + export type DraftComment = { path: string body: string diff --git a/packages/core/test/integration/engine-check-run-annotations-list.integration.test.ts b/packages/core/test/integration/engine-check-run-annotations-list.integration.test.ts deleted file mode 100644 index 01d0c78d..00000000 --- a/packages/core/test/integration/engine-check-run-annotations-list.integration.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { TaskRequest } from "@core/core/contracts/task.js" -import { executeTask } from "@core/core/routing/engine.js" -import { createGithubClient } from "@core/gql/github-client.js" -import { describe, expect, it } from "vitest" - -describe("executeTask check_run.annotations.list", () => { - it("returns cli envelope for check_run.annotations.list", async () => { - const githubClient = createGithubClient({ - async execute(): Promise { - return {} as TData - }, - }) - - const request: TaskRequest = { - task: "check_run.annotations.list", - input: { - owner: "go-modkit", - name: "modkit", - checkRunId: 123, - }, - } - - const result = await executeTask(request, { - githubClient, - ghCliAvailable: true, - ghAuthenticated: true, - cliRunner: { - run: async () => ({ - stdout: JSON.stringify([ - { - path: "src/main.ts", - startLine: 10, - message: "Error found", - annotation_level: "failure", - }, - ]), - stderr: "", - exitCode: 0, - }), - }, - }) - - expect(result.ok).toBe(true) - expect(result.meta.route_used).toBe("cli") - expect(result.data).toEqual( - expect.objectContaining({ - items: expect.any(Array), - }), - ) - }) - - it("returns validation error envelope for missing checkRunId", async () => { - const githubClient = createGithubClient({ - async execute(): Promise { - return {} as TData - }, - }) - - const request: TaskRequest = { - task: "check_run.annotations.list", - input: { - owner: "go-modkit", - name: "modkit", - }, - } - - const result = await executeTask(request, { - githubClient, - githubToken: "test-token", - ghCliAvailable: false, - ghAuthenticated: false, - }) - - expect(result.ok).toBe(false) - expect(result.error?.code).toBe("VALIDATION") - expect(result.meta.reason).toBe("INPUT_VALIDATION") - }) -}) diff --git a/packages/core/test/integration/engine-issue-assignees-add.integration.test.ts b/packages/core/test/integration/engine-issue-assignees-add.integration.test.ts new file mode 100644 index 00000000..1fda551c --- /dev/null +++ b/packages/core/test/integration/engine-issue-assignees-add.integration.test.ts @@ -0,0 +1,54 @@ +import type { TaskRequest } from "@core/core/contracts/task.js" +import { executeTask } from "@core/core/routing/engine.js" +import { createGithubClient } from "@core/gql/github-client.js" +import { describe, expect, it } from "vitest" + +describe("executeTask issue.assignees.add", () => { + it("returns validation error envelope for missing issueNumber", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.assignees.add", + input: { owner: "acme", name: "modkit", assignees: ["user1"] }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) + + it("returns validation error envelope for missing assignees", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.assignees.add", + input: { owner: "acme", name: "modkit", issueNumber: 42 }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) +}) diff --git a/packages/core/test/integration/engine-issue-assignees-remove.integration.test.ts b/packages/core/test/integration/engine-issue-assignees-remove.integration.test.ts new file mode 100644 index 00000000..45818865 --- /dev/null +++ b/packages/core/test/integration/engine-issue-assignees-remove.integration.test.ts @@ -0,0 +1,54 @@ +import type { TaskRequest } from "@core/core/contracts/task.js" +import { executeTask } from "@core/core/routing/engine.js" +import { createGithubClient } from "@core/gql/github-client.js" +import { describe, expect, it } from "vitest" + +describe("executeTask issue.assignees.remove", () => { + it("returns validation error envelope for missing issueNumber", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.assignees.remove", + input: { owner: "acme", name: "modkit", assignees: ["user1"] }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) + + it("returns validation error envelope for missing assignees", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.assignees.remove", + input: { owner: "acme", name: "modkit", issueNumber: 42 }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) +}) diff --git a/packages/core/test/integration/engine-issue-assignees-update.integration.test.ts b/packages/core/test/integration/engine-issue-assignees-set.integration.test.ts similarity index 90% rename from packages/core/test/integration/engine-issue-assignees-update.integration.test.ts rename to packages/core/test/integration/engine-issue-assignees-set.integration.test.ts index 531616a3..21790cea 100644 --- a/packages/core/test/integration/engine-issue-assignees-update.integration.test.ts +++ b/packages/core/test/integration/engine-issue-assignees-set.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask issue.assignees.update", () => { +describe("executeTask issue.assignees.set", () => { it("returns validation error envelope for missing issueId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask issue.assignees.update", () => { }) const request: TaskRequest = { - task: "issue.assignees.update", + task: "issue.assignees.set", input: { logins: ["user1"] }, } diff --git a/packages/core/test/integration/engine-issue-blocked-by-add.integration.test.ts b/packages/core/test/integration/engine-issue-blocked-by-add.integration.test.ts index 32c637dd..1dfb346f 100644 --- a/packages/core/test/integration/engine-issue-blocked-by-add.integration.test.ts +++ b/packages/core/test/integration/engine-issue-blocked-by-add.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask issue.blocked_by.add", () => { +describe("executeTask issue.relations.blocked_by.add", () => { it("returns validation error envelope for missing blockedByIssueId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask issue.blocked_by.add", () => { }) const request: TaskRequest = { - task: "issue.blocked_by.add", + task: "issue.relations.blocked_by.add", input: { issueId: "issue-id-123" }, } diff --git a/packages/core/test/integration/engine-issue-blocked-by-remove.integration.test.ts b/packages/core/test/integration/engine-issue-blocked-by-remove.integration.test.ts index 0ad6dcb7..2ff65202 100644 --- a/packages/core/test/integration/engine-issue-blocked-by-remove.integration.test.ts +++ b/packages/core/test/integration/engine-issue-blocked-by-remove.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask issue.blocked_by.remove", () => { +describe("executeTask issue.relations.blocked_by.remove", () => { it("returns validation error envelope for missing blockedByIssueId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask issue.blocked_by.remove", () => { }) const request: TaskRequest = { - task: "issue.blocked_by.remove", + task: "issue.relations.blocked_by.remove", input: { issueId: "issue-id-123" }, } diff --git a/packages/core/test/integration/engine-issue-labels-remove.integration.test.ts b/packages/core/test/integration/engine-issue-labels-remove.integration.test.ts new file mode 100644 index 00000000..3a24e2e9 --- /dev/null +++ b/packages/core/test/integration/engine-issue-labels-remove.integration.test.ts @@ -0,0 +1,54 @@ +import type { TaskRequest } from "@core/core/contracts/task.js" +import { executeTask } from "@core/core/routing/engine.js" +import { createGithubClient } from "@core/gql/github-client.js" +import { describe, expect, it } from "vitest" + +describe("executeTask issue.labels.remove", () => { + it("returns validation error envelope for missing issueNumber", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.labels.remove", + input: { owner: "acme", name: "modkit", labels: ["bug"] }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) + + it("returns validation error envelope for missing labels", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.labels.remove", + input: { owner: "acme", name: "modkit", issueNumber: 42 }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) +}) diff --git a/packages/core/test/integration/engine-issue-labels-update.integration.test.ts b/packages/core/test/integration/engine-issue-labels-update.integration.test.ts index a46ff506..a1f53622 100644 --- a/packages/core/test/integration/engine-issue-labels-update.integration.test.ts +++ b/packages/core/test/integration/engine-issue-labels-update.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask issue.labels.update", () => { +describe("executeTask issue.labels.set", () => { it("returns validation error envelope for missing issueId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask issue.labels.update", () => { }) const request: TaskRequest = { - task: "issue.labels.update", + task: "issue.labels.set", input: { labelNames: ["bug"] }, } diff --git a/packages/core/test/integration/engine-issue-milestone-clear.integration.test.ts b/packages/core/test/integration/engine-issue-milestone-clear.integration.test.ts new file mode 100644 index 00000000..6cf61588 --- /dev/null +++ b/packages/core/test/integration/engine-issue-milestone-clear.integration.test.ts @@ -0,0 +1,53 @@ +import type { TaskRequest } from "@core/core/contracts/task.js" +import { executeTask } from "@core/core/routing/engine.js" +import { createGithubClient } from "@core/gql/github-client.js" +import { describe, expect, it } from "vitest" + +describe("executeTask issue.milestone.clear", () => { + it("returns validation error envelope for missing issueNumber", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.milestone.clear", + input: { owner: "acme", name: "modkit" }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) + + it("returns adapter-unsupported when CLI unavailable", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "issue.milestone.clear", + input: { owner: "acme", name: "modkit", issueNumber: 42 }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("ADAPTER_UNSUPPORTED") + }) +}) diff --git a/packages/core/test/integration/engine-issue-parent-remove.integration.test.ts b/packages/core/test/integration/engine-issue-parent-remove.integration.test.ts index 414ea232..d74638b5 100644 --- a/packages/core/test/integration/engine-issue-parent-remove.integration.test.ts +++ b/packages/core/test/integration/engine-issue-parent-remove.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask issue.parent.remove", () => { +describe("executeTask issue.relations.parent.remove", () => { it("returns validation error envelope for missing issueId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask issue.parent.remove", () => { }) const request: TaskRequest = { - task: "issue.parent.remove", + task: "issue.relations.parent.remove", input: {}, } diff --git a/packages/core/test/integration/engine-issue-parent-set.integration.test.ts b/packages/core/test/integration/engine-issue-parent-set.integration.test.ts index 1e052920..89e3cb1e 100644 --- a/packages/core/test/integration/engine-issue-parent-set.integration.test.ts +++ b/packages/core/test/integration/engine-issue-parent-set.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask issue.parent.set", () => { +describe("executeTask issue.relations.parent.set", () => { it("returns validation error envelope for missing parentIssueId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask issue.parent.set", () => { }) const request: TaskRequest = { - task: "issue.parent.set", + task: "issue.relations.parent.set", input: { issueId: "issue-id-123" }, } diff --git a/packages/core/test/integration/engine-issue-relations-get.integration.test.ts b/packages/core/test/integration/engine-issue-relations-get.integration.test.ts index 4b483612..dae78c55 100644 --- a/packages/core/test/integration/engine-issue-relations-get.integration.test.ts +++ b/packages/core/test/integration/engine-issue-relations-get.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask issue.relations.get", () => { +describe("executeTask issue.relations.view", () => { it("returns validation error envelope for invalid issueNumber", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask issue.relations.get", () => { }) const request: TaskRequest = { - task: "issue.relations.get", + task: "issue.relations.view", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-issue-linked-prs-list.integration.test.ts b/packages/core/test/integration/engine-issue-relations-prs-list.integration.test.ts similarity index 91% rename from packages/core/test/integration/engine-issue-linked-prs-list.integration.test.ts rename to packages/core/test/integration/engine-issue-relations-prs-list.integration.test.ts index c3e7066d..c8c5b88a 100644 --- a/packages/core/test/integration/engine-issue-linked-prs-list.integration.test.ts +++ b/packages/core/test/integration/engine-issue-relations-prs-list.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it, vi } from "vitest" -describe("executeTask issue.linked_prs.list", () => { - it("returns graphql envelope for issue.linked_prs.list", async () => { +describe("executeTask issue.relations.prs.list", () => { + it("returns graphql envelope for issue.relations.prs.list", async () => { const githubClient = createGithubClient({ async execute(query: string): Promise { if (query.includes("query IssueLinkedPrs")) { @@ -37,7 +37,7 @@ describe("executeTask issue.linked_prs.list", () => { }) const request: TaskRequest = { - task: "issue.linked_prs.list", + task: "issue.relations.prs.list", input: { owner: "go-modkit", name: "modkit", @@ -68,7 +68,7 @@ describe("executeTask issue.linked_prs.list", () => { }) const request: TaskRequest = { - task: "issue.linked_prs.list", + task: "issue.relations.prs.list", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-pr-checks-get-failed.integration.test.ts b/packages/core/test/integration/engine-pr-assignees-add.integration.test.ts similarity index 53% rename from packages/core/test/integration/engine-pr-checks-get-failed.integration.test.ts rename to packages/core/test/integration/engine-pr-assignees-add.integration.test.ts index 42daac4d..f1d8c3ac 100644 --- a/packages/core/test/integration/engine-pr-checks-get-failed.integration.test.ts +++ b/packages/core/test/integration/engine-pr-assignees-add.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.checks.failed", () => { - it("returns cli envelope for pr.checks.failed", async () => { +describe("executeTask pr.assignees.add", () => { + it("returns validation error envelope for missing prNumber", async () => { const githubClient = createGithubClient({ async execute(): Promise { return {} as TData @@ -12,38 +12,23 @@ describe("executeTask pr.checks.failed", () => { }) const request: TaskRequest = { - task: "pr.checks.failed", - input: { - owner: "go-modkit", - name: "modkit", - prNumber: 232, - }, + task: "pr.assignees.add", + input: { owner: "acme", name: "modkit", assignees: ["user1"] }, } const result = await executeTask(request, { githubClient, - ghCliAvailable: true, - ghAuthenticated: true, - cliRunner: { - run: async () => ({ - stdout: JSON.stringify([ - { - name: "test", - status: "completed", - conclusion: "failure", - }, - ]), - stderr: "", - exitCode: 0, - }), - }, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, }) - expect(result.ok).toBe(true) - expect(result.meta.route_used).toBe("cli") + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") }) - it("returns validation error envelope for invalid prNumber", async () => { + it("returns validation error envelope for missing assignees", async () => { const githubClient = createGithubClient({ async execute(): Promise { return {} as TData @@ -51,12 +36,8 @@ describe("executeTask pr.checks.failed", () => { }) const request: TaskRequest = { - task: "pr.checks.failed", - input: { - owner: "go-modkit", - name: "modkit", - prNumber: 0, - }, + task: "pr.assignees.add", + input: { owner: "acme", name: "modkit", prNumber: 10 }, } const result = await executeTask(request, { diff --git a/packages/core/test/integration/engine-pr-assignees-remove.integration.test.ts b/packages/core/test/integration/engine-pr-assignees-remove.integration.test.ts new file mode 100644 index 00000000..a9adcc14 --- /dev/null +++ b/packages/core/test/integration/engine-pr-assignees-remove.integration.test.ts @@ -0,0 +1,54 @@ +import type { TaskRequest } from "@core/core/contracts/task.js" +import { executeTask } from "@core/core/routing/engine.js" +import { createGithubClient } from "@core/gql/github-client.js" +import { describe, expect, it } from "vitest" + +describe("executeTask pr.assignees.remove", () => { + it("returns validation error envelope for missing prNumber", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "pr.assignees.remove", + input: { owner: "acme", name: "modkit", assignees: ["user1"] }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) + + it("returns validation error envelope for missing assignees", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "pr.assignees.remove", + input: { owner: "acme", name: "modkit", prNumber: 10 }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) +}) diff --git a/packages/core/test/integration/engine-pr-assignees-update.integration.test.ts b/packages/core/test/integration/engine-pr-assignees-update.integration.test.ts deleted file mode 100644 index 725332c3..00000000 --- a/packages/core/test/integration/engine-pr-assignees-update.integration.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { TaskRequest } from "@core/core/contracts/task.js" -import { executeTask } from "@core/core/routing/engine.js" -import { createGithubClient } from "@core/gql/github-client.js" -import { describe, expect, it } from "vitest" - -describe("executeTask pr.assignees.update", () => { - it("returns validation error envelope for invalid prNumber", async () => { - const githubClient = createGithubClient({ - async execute(): Promise { - return {} as TData - }, - }) - - const request: TaskRequest = { - task: "pr.assignees.update", - input: { - owner: "go-modkit", - name: "modkit", - prNumber: 0, - logins: ["user1"], - }, - } - - const result = await executeTask(request, { - githubClient, - githubToken: "test-token", - ghCliAvailable: false, - ghAuthenticated: false, - }) - - expect(result.ok).toBe(false) - expect(result.error?.code).toBe("VALIDATION") - expect(result.meta.reason).toBe("INPUT_VALIDATION") - }) -}) diff --git a/packages/core/test/integration/engine-pr-checks-rerun-all.integration.test.ts b/packages/core/test/integration/engine-pr-checks-rerun-all.integration.test.ts index 0b6a94b4..4adf5051 100644 --- a/packages/core/test/integration/engine-pr-checks-rerun-all.integration.test.ts +++ b/packages/core/test/integration/engine-pr-checks-rerun-all.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.checks.rerun_all", () => { +describe("executeTask pr.checks.rerun.all", () => { it("returns validation error envelope for invalid prNumber", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask pr.checks.rerun_all", () => { }) const request: TaskRequest = { - task: "pr.checks.rerun_all", + task: "pr.checks.rerun.all", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-pr-checks-rerun-failed.integration.test.ts b/packages/core/test/integration/engine-pr-checks-rerun-failed.integration.test.ts index 5af66ca3..2008c16d 100644 --- a/packages/core/test/integration/engine-pr-checks-rerun-failed.integration.test.ts +++ b/packages/core/test/integration/engine-pr-checks-rerun-failed.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.checks.rerun_failed", () => { +describe("executeTask pr.checks.rerun.failed", () => { it("returns validation error envelope for invalid prNumber", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask pr.checks.rerun_failed", () => { }) const request: TaskRequest = { - task: "pr.checks.rerun_failed", + task: "pr.checks.rerun.failed", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-pr-comment-reply.integration.test.ts b/packages/core/test/integration/engine-pr-comment-reply.integration.test.ts index 0f47db4c..71cdc819 100644 --- a/packages/core/test/integration/engine-pr-comment-reply.integration.test.ts +++ b/packages/core/test/integration/engine-pr-comment-reply.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.thread.reply", () => { +describe("executeTask pr.threads.reply", () => { it("returns validation error envelope for missing body", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask pr.thread.reply", () => { }) const request: TaskRequest = { - task: "pr.thread.reply", + task: "pr.threads.reply", input: { commentId: "comment-id-123" }, } diff --git a/packages/core/test/integration/engine-pr-comment-resolve.integration.test.ts b/packages/core/test/integration/engine-pr-comment-resolve.integration.test.ts index 187d010b..dbf0e275 100644 --- a/packages/core/test/integration/engine-pr-comment-resolve.integration.test.ts +++ b/packages/core/test/integration/engine-pr-comment-resolve.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.thread.resolve", () => { +describe("executeTask pr.threads.resolve", () => { it("returns validation error envelope for missing threadId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask pr.thread.resolve", () => { }) const request: TaskRequest = { - task: "pr.thread.resolve", + task: "pr.threads.resolve", input: {}, } diff --git a/packages/core/test/integration/engine-pr-comment-unresolve.integration.test.ts b/packages/core/test/integration/engine-pr-comment-unresolve.integration.test.ts index 1df7f256..e1bde0cb 100644 --- a/packages/core/test/integration/engine-pr-comment-unresolve.integration.test.ts +++ b/packages/core/test/integration/engine-pr-comment-unresolve.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.thread.unresolve", () => { +describe("executeTask pr.threads.unresolve", () => { it("returns validation error envelope for missing threadId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask pr.thread.unresolve", () => { }) const request: TaskRequest = { - task: "pr.thread.unresolve", + task: "pr.threads.unresolve", input: {}, } diff --git a/packages/core/test/integration/engine-pr-review-request.integration.test.ts b/packages/core/test/integration/engine-pr-review-request.integration.test.ts index 7e69abe1..4f61634d 100644 --- a/packages/core/test/integration/engine-pr-review-request.integration.test.ts +++ b/packages/core/test/integration/engine-pr-review-request.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.review.request", () => { +describe("executeTask pr.reviews.request", () => { it("returns validation error envelope for missing logins", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask pr.review.request", () => { }) const request: TaskRequest = { - task: "pr.review.request", + task: "pr.reviews.request", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-pr-review-submit-approve.integration.test.ts b/packages/core/test/integration/engine-pr-review-submit-approve.integration.test.ts index 5f5e63f1..506f57d1 100644 --- a/packages/core/test/integration/engine-pr-review-submit-approve.integration.test.ts +++ b/packages/core/test/integration/engine-pr-review-submit-approve.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import type { GithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.review.submit", () => { - it("returns graphql envelope for pr.review.submit", async () => { +describe("executeTask pr.reviews.submit", () => { + it("returns graphql envelope for pr.reviews.submit", async () => { const githubClient = { submitPrReview: async () => ({ id: "review-id-123", @@ -15,7 +15,7 @@ describe("executeTask pr.review.submit", () => { } as unknown as GithubClient const request: TaskRequest = { - task: "pr.review.submit", + task: "pr.reviews.submit", input: { owner: "go-modkit", name: "modkit", @@ -38,7 +38,7 @@ describe("executeTask pr.review.submit", () => { const githubClient = {} as GithubClient const request: TaskRequest = { - task: "pr.review.submit", + task: "pr.reviews.submit", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-pr-reviews-list.integration.test.ts b/packages/core/test/integration/engine-pr-reviews-list.integration.test.ts index 5a8e6a27..30f5d8c8 100644 --- a/packages/core/test/integration/engine-pr-reviews-list.integration.test.ts +++ b/packages/core/test/integration/engine-pr-reviews-list.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask pr.review.list", () => { +describe("executeTask pr.reviews.list", () => { it("returns validation error envelope for missing prNumber", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask pr.review.list", () => { }) const request: TaskRequest = { - task: "pr.review.list", + task: "pr.reviews.list", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-project-v2-item-add-issue.integration.test.ts b/packages/core/test/integration/engine-project-v2-item-add-issue.integration.test.ts index 83a50c00..1cddc5e1 100644 --- a/packages/core/test/integration/engine-project-v2-item-add-issue.integration.test.ts +++ b/packages/core/test/integration/engine-project-v2-item-add-issue.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask project_v2.item.add_issue", () => { +describe("executeTask project_v2.items.issue.add", () => { it("returns validation error envelope for missing contentId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask project_v2.item.add_issue", () => { }) const request: TaskRequest = { - task: "project_v2.item.add_issue", + task: "project_v2.items.issue.add", input: { projectId: "project-id-1" }, } diff --git a/packages/core/test/integration/engine-project-v2-item-field-update.integration.test.ts b/packages/core/test/integration/engine-project-v2-item-field-update.integration.test.ts index 3a58e3a6..6dda2ff9 100644 --- a/packages/core/test/integration/engine-project-v2-item-field-update.integration.test.ts +++ b/packages/core/test/integration/engine-project-v2-item-field-update.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask project_v2.item.field.update", () => { +describe("executeTask project_v2.items.field.update", () => { it("returns adapter unsupported when CLI unavailable", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask project_v2.item.field.update", () => { }) const request: TaskRequest = { - task: "project_v2.item.field.update", + task: "project_v2.items.field.update", input: { projectId: "project-id-1", itemId: "item-id-1", diff --git a/packages/core/test/integration/engine-project-v2-items-issue-remove.integration.test.ts b/packages/core/test/integration/engine-project-v2-items-issue-remove.integration.test.ts new file mode 100644 index 00000000..84f551ae --- /dev/null +++ b/packages/core/test/integration/engine-project-v2-items-issue-remove.integration.test.ts @@ -0,0 +1,77 @@ +import type { TaskRequest } from "@core/core/contracts/task.js" +import { executeTask } from "@core/core/routing/engine.js" +import { createGithubClient } from "@core/gql/github-client.js" +import { describe, expect, it } from "vitest" + +describe("executeTask project_v2.items.issue.remove", () => { + it("returns validation error envelope for missing projectNumber", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "project_v2.items.issue.remove", + input: { owner: "myorg", itemId: "PVTI_abc123" }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) + + it("returns validation error envelope for missing itemId", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "project_v2.items.issue.remove", + input: { owner: "myorg", projectNumber: 1 }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("VALIDATION") + expect(result.meta.reason).toBe("INPUT_VALIDATION") + }) + + it("returns adapter-unsupported when CLI unavailable", async () => { + const githubClient = createGithubClient({ + async execute(): Promise { + return {} as TData + }, + }) + + const request: TaskRequest = { + task: "project_v2.items.issue.remove", + input: { owner: "myorg", projectNumber: 1, itemId: "PVTI_abc123" }, + } + + const result = await executeTask(request, { + githubClient, + githubToken: "test-token", + ghCliAvailable: false, + ghAuthenticated: false, + }) + + expect(result.ok).toBe(false) + expect(result.error?.code).toBe("ADAPTER_UNSUPPORTED") + }) +}) diff --git a/packages/core/test/integration/engine-project-v2-org-get.integration.test.ts b/packages/core/test/integration/engine-project-v2-org-get.integration.test.ts index ff5bc5df..27c631a6 100644 --- a/packages/core/test/integration/engine-project-v2-org-get.integration.test.ts +++ b/packages/core/test/integration/engine-project-v2-org-get.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask project_v2.org.get", () => { +describe("executeTask project_v2.org.view", () => { it("returns validation error envelope for missing number", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask project_v2.org.get", () => { }) const request: TaskRequest = { - task: "project_v2.org.get", + task: "project_v2.org.view", input: { org: "my-org" }, } diff --git a/packages/core/test/integration/engine-project-v2-user-get.integration.test.ts b/packages/core/test/integration/engine-project-v2-user-get.integration.test.ts index 866c4f96..45eb0117 100644 --- a/packages/core/test/integration/engine-project-v2-user-get.integration.test.ts +++ b/packages/core/test/integration/engine-project-v2-user-get.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask project_v2.user.get", () => { +describe("executeTask project_v2.user.view", () => { it("returns validation error envelope for missing number", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask project_v2.user.get", () => { }) const request: TaskRequest = { - task: "project_v2.user.get", + task: "project_v2.user.view", input: { user: "my-user" }, } diff --git a/packages/core/test/integration/engine-release-create-draft.integration.test.ts b/packages/core/test/integration/engine-release-create-draft.integration.test.ts index 9ee99188..15fff744 100644 --- a/packages/core/test/integration/engine-release-create-draft.integration.test.ts +++ b/packages/core/test/integration/engine-release-create-draft.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask release.create_draft", () => { - it("returns cli envelope for release.create_draft", async () => { +describe("executeTask release.create", () => { + it("returns cli envelope for release.create", async () => { const githubClient = createGithubClient({ async execute(): Promise { return {} as TData @@ -12,7 +12,7 @@ describe("executeTask release.create_draft", () => { }) const request: TaskRequest = { - task: "release.create_draft", + task: "release.create", input: { owner: "go-modkit", name: "modkit", @@ -56,7 +56,7 @@ describe("executeTask release.create_draft", () => { }) const request: TaskRequest = { - task: "release.create_draft", + task: "release.create", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-release-publish-draft.integration.test.ts b/packages/core/test/integration/engine-release-publish-draft.integration.test.ts index 6613ec74..242240de 100644 --- a/packages/core/test/integration/engine-release-publish-draft.integration.test.ts +++ b/packages/core/test/integration/engine-release-publish-draft.integration.test.ts @@ -3,7 +3,7 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask release.publish_draft", () => { +describe("executeTask release.publish", () => { it("returns validation error envelope for missing releaseId", async () => { const githubClient = createGithubClient({ async execute(): Promise { @@ -12,7 +12,7 @@ describe("executeTask release.publish_draft", () => { }) const request: TaskRequest = { - task: "release.publish_draft", + task: "release.publish", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-workflow-dispatch-run.integration.test.ts b/packages/core/test/integration/engine-workflow-dispatch-run.integration.test.ts index 0d72889c..34c2f3cc 100644 --- a/packages/core/test/integration/engine-workflow-dispatch-run.integration.test.ts +++ b/packages/core/test/integration/engine-workflow-dispatch-run.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask workflow.dispatch.run", () => { - it("returns cli envelope for workflow.dispatch.run", async () => { +describe("executeTask workflow.dispatch", () => { + it("returns cli envelope for workflow.dispatch", async () => { const githubClient = createGithubClient({ async execute(): Promise { return {} as TData @@ -12,7 +12,7 @@ describe("executeTask workflow.dispatch.run", () => { }) const request: TaskRequest = { - task: "workflow.dispatch.run", + task: "workflow.dispatch", input: { owner: "go-modkit", name: "modkit", @@ -50,7 +50,7 @@ describe("executeTask workflow.dispatch.run", () => { }) const request: TaskRequest = { - task: "workflow.dispatch.run", + task: "workflow.dispatch", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-workflow-job-logs-analyze.integration.test.ts b/packages/core/test/integration/engine-workflow-job-logs-analyze.integration.test.ts index 64ca782c..5619458b 100644 --- a/packages/core/test/integration/engine-workflow-job-logs-analyze.integration.test.ts +++ b/packages/core/test/integration/engine-workflow-job-logs-analyze.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask workflow.job.logs.get", () => { - it("returns cli envelope for workflow.job.logs.get", async () => { +describe("executeTask workflow.job.logs.view", () => { + it("returns cli envelope for workflow.job.logs.view", async () => { const githubClient = createGithubClient({ async execute(): Promise { return {} as TData @@ -12,7 +12,7 @@ describe("executeTask workflow.job.logs.get", () => { }) const request: TaskRequest = { - task: "workflow.job.logs.get", + task: "workflow.job.logs.view", input: { owner: "go-modkit", name: "modkit", @@ -52,7 +52,7 @@ describe("executeTask workflow.job.logs.get", () => { }) const request: TaskRequest = { - task: "workflow.job.logs.get", + task: "workflow.job.logs.view", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-workflow-run-rerun-all.integration.test.ts b/packages/core/test/integration/engine-workflow-run-rerun-all.integration.test.ts index 51ae806d..b7025916 100644 --- a/packages/core/test/integration/engine-workflow-run-rerun-all.integration.test.ts +++ b/packages/core/test/integration/engine-workflow-run-rerun-all.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask workflow.run.rerun_all", () => { - it("returns cli envelope for workflow.run.rerun_all", async () => { +describe("executeTask workflow.run.rerun.all", () => { + it("returns cli envelope for workflow.run.rerun.all", async () => { const githubClient = createGithubClient({ async execute(): Promise { return {} as TData @@ -12,7 +12,7 @@ describe("executeTask workflow.run.rerun_all", () => { }) const request: TaskRequest = { - task: "workflow.run.rerun_all", + task: "workflow.run.rerun.all", input: { owner: "go-modkit", name: "modkit", @@ -45,7 +45,7 @@ describe("executeTask workflow.run.rerun_all", () => { }) const request: TaskRequest = { - task: "workflow.run.rerun_all", + task: "workflow.run.rerun.all", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/integration/engine-workflow-run-rerun-failed.integration.test.ts b/packages/core/test/integration/engine-workflow-run-rerun-failed.integration.test.ts index 6b31c4dd..1bb57f34 100644 --- a/packages/core/test/integration/engine-workflow-run-rerun-failed.integration.test.ts +++ b/packages/core/test/integration/engine-workflow-run-rerun-failed.integration.test.ts @@ -3,8 +3,8 @@ import { executeTask } from "@core/core/routing/engine.js" import { createGithubClient } from "@core/gql/github-client.js" import { describe, expect, it } from "vitest" -describe("executeTask workflow.run.rerun_failed", () => { - it("returns cli envelope for workflow.run.rerun_failed", async () => { +describe("executeTask workflow.run.rerun.failed", () => { + it("returns cli envelope for workflow.run.rerun.failed", async () => { const githubClient = createGithubClient({ async execute(): Promise { return {} as TData @@ -12,7 +12,7 @@ describe("executeTask workflow.run.rerun_failed", () => { }) const request: TaskRequest = { - task: "workflow.run.rerun_failed", + task: "workflow.run.rerun.failed", input: { owner: "go-modkit", name: "modkit", @@ -45,7 +45,7 @@ describe("executeTask workflow.run.rerun_failed", () => { }) const request: TaskRequest = { - task: "workflow.run.rerun_failed", + task: "workflow.run.rerun.failed", input: { owner: "go-modkit", name: "modkit", diff --git a/packages/core/test/unit/capability-registry.test.ts b/packages/core/test/unit/capability-registry.test.ts index d4548781..4540aa9d 100644 --- a/packages/core/test/unit/capability-registry.test.ts +++ b/packages/core/test/unit/capability-registry.test.ts @@ -1,4 +1,3 @@ -import { getOperationCard, listOperationCards } from "@core/core/registry/index.js" import { capabilityRegistry } from "@core/core/routing/capability-registry.js" import { describe, expect, it } from "vitest" @@ -20,16 +19,6 @@ describe("capabilityRegistry", () => { defaultRoute: "cli", fallbackRoutes: [], }, - { - task: "issue.triage.composite", - defaultRoute: "graphql", - fallbackRoutes: [], - }, - { - task: "issue.update.composite", - defaultRoute: "graphql", - fallbackRoutes: [], - }, { task: "issue.view", defaultRoute: "graphql", @@ -71,7 +60,7 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "issue.labels.update", + task: "issue.labels.set", defaultRoute: "graphql", fallbackRoutes: [], }, @@ -81,52 +70,67 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "issue.assignees.update", + task: "issue.labels.remove", + defaultRoute: "cli", + fallbackRoutes: [], + }, + { + task: "issue.assignees.set", defaultRoute: "graphql", fallbackRoutes: [], }, + { + task: "issue.assignees.add", + defaultRoute: "cli", + fallbackRoutes: [], + }, + { + task: "issue.assignees.remove", + defaultRoute: "cli", + fallbackRoutes: [], + }, { task: "issue.milestone.set", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "issue.comments.create", - defaultRoute: "graphql", + task: "issue.milestone.clear", + defaultRoute: "cli", fallbackRoutes: [], }, { - task: "issue.linked_prs.list", + task: "issue.comments.create", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "issue.relations.get", + task: "issue.relations.prs.list", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "issue.parent.set", + task: "issue.relations.view", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "issue.parent.remove", + task: "issue.relations.parent.set", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "issue.blocked_by.add", + task: "issue.relations.parent.remove", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "issue.blocked_by.remove", + task: "issue.relations.blocked_by.add", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "pr.threads.composite", + task: "issue.relations.blocked_by.remove", defaultRoute: "graphql", fallbackRoutes: [], }, @@ -151,37 +155,37 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "pr.thread.list", + task: "pr.threads.list", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "pr.thread.reply", + task: "pr.threads.reply", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "pr.thread.resolve", + task: "pr.threads.resolve", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "pr.thread.unresolve", + task: "pr.threads.unresolve", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "pr.review.list", + task: "pr.reviews.list", defaultRoute: "graphql", fallbackRoutes: [], }, { - task: "pr.review.request", + task: "pr.reviews.request", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "pr.review.submit", + task: "pr.reviews.submit", defaultRoute: "graphql", fallbackRoutes: [], }, @@ -201,17 +205,12 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "pr.checks.failed", - defaultRoute: "cli", - fallbackRoutes: [], - }, - { - task: "pr.checks.rerun_failed", + task: "pr.checks.rerun.failed", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "pr.checks.rerun_all", + task: "pr.checks.rerun.all", defaultRoute: "cli", fallbackRoutes: [], }, @@ -226,17 +225,17 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "pr.assignees.update", + task: "pr.assignees.add", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "pr.branch.update", + task: "pr.assignees.remove", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "check_run.annotations.list", + task: "pr.branch.update", defaultRoute: "cli", fallbackRoutes: [], }, @@ -246,17 +245,17 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "workflow.get", + task: "workflow.view", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "project_v2.org.get", + task: "project_v2.org.view", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "project_v2.user.get", + task: "project_v2.user.view", defaultRoute: "cli", fallbackRoutes: [], }, @@ -271,12 +270,17 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "project_v2.item.add_issue", + task: "project_v2.items.issue.add", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "project_v2.item.field.update", + task: "project_v2.items.issue.remove", + defaultRoute: "cli", + fallbackRoutes: [], + }, + { + task: "project_v2.items.field.update", defaultRoute: "cli", fallbackRoutes: [], }, @@ -286,12 +290,12 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "release.get", + task: "release.view", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "release.create_draft", + task: "release.create", defaultRoute: "cli", fallbackRoutes: [], }, @@ -301,17 +305,17 @@ describe("capabilityRegistry", () => { fallbackRoutes: [], }, { - task: "release.publish_draft", + task: "release.publish", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "workflow.dispatch.run", + task: "workflow.dispatch", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "workflow.job.logs.get", + task: "workflow.job.logs.view", defaultRoute: "cli", fallbackRoutes: [], }, @@ -332,12 +336,12 @@ describe("capabilityRegistry", () => { }, { - task: "workflow.run.rerun_all", + task: "workflow.run.rerun.all", defaultRoute: "cli", fallbackRoutes: [], }, { - task: "workflow.run.rerun_failed", + task: "workflow.run.rerun.failed", defaultRoute: "cli", fallbackRoutes: [], }, @@ -354,39 +358,3 @@ describe("capabilityRegistry", () => { ]) }) }) - -describe("composite capability cards", () => { - it("loads issue composite cards with composite config", () => { - const triageCard = getOperationCard("issue.triage.composite") - const updateCard = getOperationCard("issue.update.composite") - - expect(triageCard).toBeDefined() - expect(updateCard).toBeDefined() - expect(triageCard?.composite).toBeDefined() - expect(updateCard?.composite).toBeDefined() - expect(triageCard?.routing.preferred).toBe("graphql") - expect(updateCard?.routing.preferred).toBe("graphql") - }) - - it("loads pr.threads.composite card with composite config", () => { - const card = getOperationCard("pr.threads.composite") - expect(card).toBeDefined() - if (!card) return - expect(card.composite).toBeDefined() - expect(card.routing.preferred).toBe("graphql") - if (!card.composite) return - expect(card.composite.output_strategy).toBe("array") - expect(card.composite.steps.length).toBeGreaterThan(0) - }) - - it("lists pr.threads.composite before pr.view", () => { - const cards = listOperationCards() - const triageIdx = cards.findIndex((c) => c.capability_id === "issue.triage.composite") - const issueViewIdx = cards.findIndex((c) => c.capability_id === "issue.view") - const compositeIdx = cards.findIndex((c) => c.capability_id === "pr.threads.composite") - const viewIdx = cards.findIndex((c) => c.capability_id === "pr.view") - - expect(triageIdx).toBeLessThan(issueViewIdx) - expect(compositeIdx).toBeLessThan(viewIdx) - }) -}) diff --git a/packages/core/test/unit/cli-capability-adapter.test.ts b/packages/core/test/unit/cli-capability-adapter.test.ts index 74386025..94a05af8 100644 --- a/packages/core/test/unit/cli-capability-adapter.test.ts +++ b/packages/core/test/unit/cli-capability-adapter.test.ts @@ -667,7 +667,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "project_v2.item.field.update", { + const result = await runCliCapability(runner, "project_v2.items.field.update", { projectId: "PVT_kwDO123", itemId: "ITEM_123", fieldId: "FIELD_123", @@ -678,7 +678,7 @@ describe("runCliCapability", () => { expect(result.error?.message).toBe("gh command failed; stderr redacted for safety") expect(result.error?.details).toEqual( expect.objectContaining({ - capabilityId: "project_v2.item.field.update", + capabilityId: "project_v2.items.field.update", exitCode: 1, }), ) @@ -733,45 +733,6 @@ describe("runCliCapability", () => { ) }) - it("filters failed checks for pr.checks.failed", async () => { - const runner = { - run: vi.fn(async () => ({ - stdout: JSON.stringify([ - { - name: "unit-tests", - state: "SUCCESS", - bucket: "pass", - workflow: "ci", - link: "https://example.com/check/1", - }, - { - name: "lint", - state: "FAILURE", - bucket: "fail", - workflow: "ci", - link: "https://example.com/check/2", - }, - ]), - stderr: "", - exitCode: 0, - })), - } - - const result = await runCliCapability(runner, "pr.checks.failed", { - owner: "acme", - name: "modkit", - prNumber: 10, - }) - - expect(result.ok).toBe(true) - expect(result.data).toEqual( - expect.objectContaining({ - items: [expect.objectContaining({ name: "lint", state: "FAILURE" })], - summary: expect.objectContaining({ total: 2, failed: 1 }), - }), - ) - }) - it("normalizes mergeability fields for pr.merge.status", async () => { const runner = { run: vi.fn(async () => ({ @@ -908,36 +869,35 @@ describe("runCliCapability", () => { })), } - const rerunFailed = await runCliCapability(runner, "pr.checks.rerun_failed", { + const rerunFailed = await runCliCapability(runner, "pr.checks.rerun.failed", { owner: "acme", name: "modkit", prNumber: 10, runId: 88, }) - const rerunAll = await runCliCapability(runner, "pr.checks.rerun_all", { + const rerunAll = await runCliCapability(runner, "pr.checks.rerun.all", { owner: "acme", name: "modkit", prNumber: 10, runId: 88, }) - const reviewers = await runCliCapability(runner, "pr.review.request", { + const reviewers = await runCliCapability(runner, "pr.reviews.request", { owner: "acme", name: "modkit", prNumber: 10, reviewers: ["octocat", "hubot"], }) - const assignees = await runCliCapability(runner, "pr.assignees.update", { + const assigneesAdd = await runCliCapability(runner, "pr.assignees.add", { owner: "acme", name: "modkit", prNumber: 10, - add: ["octocat"], - remove: ["hubot"], + assignees: ["octocat"], }) expect(rerunFailed.ok).toBe(true) expect(rerunAll.ok).toBe(true) expect(reviewers.ok).toBe(true) - expect(assignees.ok).toBe(true) + expect(assigneesAdd.ok).toBe(true) expect(runner.run).toHaveBeenNthCalledWith( 1, @@ -960,17 +920,7 @@ describe("runCliCapability", () => { expect(runner.run).toHaveBeenNthCalledWith( 4, "gh", - [ - "pr", - "edit", - "10", - "--repo", - "acme/modkit", - "--add-assignee", - "octocat", - "--remove-assignee", - "hubot", - ], + ["pr", "edit", "10", "--repo", "acme/modkit", "--add-assignee", "octocat"], 10_000, ) }) @@ -991,7 +941,7 @@ describe("runCliCapability", () => { }), } - const rerunFailed = await runCliCapability(runner, "pr.checks.rerun_failed", { + const rerunFailed = await runCliCapability(runner, "pr.checks.rerun.failed", { owner: "acme", name: "modkit", prNumber: 10, @@ -1000,9 +950,7 @@ describe("runCliCapability", () => { expect(rerunFailed.ok).toBe(true) expect(rerunFailed.data).toEqual({ - prNumber: 10, runId: 88, - mode: "all", queued: true, }) expect(runner.run).toHaveBeenNthCalledWith( @@ -1028,7 +976,7 @@ describe("runCliCapability", () => { })), } - const rerunResult = await runCliCapability(runner, "workflow.run.rerun_failed", { + const rerunResult = await runCliCapability(runner, "workflow.run.rerun.failed", { owner: "acme", name: "modkit", runId: 88, @@ -1037,7 +985,7 @@ describe("runCliCapability", () => { expect(rerunResult.ok).toBe(true) expect(rerunResult.data).toEqual({ runId: 88, - rerunFailed: true, + queued: true, }) }) @@ -1056,24 +1004,23 @@ describe("runCliCapability", () => { prNumber: 10, method: "fast-forward", }) - const invalidRerun = await runCliCapability(runner, "pr.checks.rerun_failed", { + const invalidRerun = await runCliCapability(runner, "pr.checks.rerun.failed", { owner: "acme", name: "modkit", prNumber: 10, runId: 0, }) - const invalidReviewers = await runCliCapability(runner, "pr.review.request", { + const invalidReviewers = await runCliCapability(runner, "pr.reviews.request", { owner: "acme", name: "modkit", prNumber: 10, reviewers: [], }) - const invalidAssignees = await runCliCapability(runner, "pr.assignees.update", { + const invalidAssignees = await runCliCapability(runner, "pr.assignees.add", { owner: "acme", name: "modkit", prNumber: 10, - add: [], - remove: [], + assignees: [], }) expect(invalidMergeMethod.ok).toBe(false) @@ -1087,59 +1034,6 @@ describe("runCliCapability", () => { expect(runner.run).not.toHaveBeenCalled() }) - it("normalizes check run annotations list", async () => { - const runner = { - run: vi.fn(async () => ({ - stdout: JSON.stringify([ - { - path: "src/index.ts", - start_line: 10, - end_line: 10, - annotation_level: "failure", - message: "Unexpected any", - title: "Type check", - raw_details: "no-explicit-any", - }, - ]), - stderr: "", - exitCode: 0, - })), - } - - const result = await runCliCapability(runner, "check_run.annotations.list", { - owner: "acme", - name: "modkit", - checkRunId: 100, - }) - - expect(result.ok).toBe(true) - expect(result.data).toEqual( - expect.objectContaining({ - items: [expect.objectContaining({ path: "src/index.ts", level: "failure" })], - }), - ) - }) - - it("returns validation error when check run annotations owner/name is missing", async () => { - const runner = { - run: vi.fn(async () => ({ - stdout: "[]", - stderr: "", - exitCode: 0, - })), - } - - const result = await runCliCapability(runner, "check_run.annotations.list", { - owner: "", - name: "", - checkRunId: 100, - }) - - expect(result.ok).toBe(false) - expect(result.error?.message).toContain("Missing owner/name for check_run.annotations.list") - expect(runner.run).not.toHaveBeenCalled() - }) - it("normalizes workflow runs list", async () => { const runner = { run: vi.fn(async () => ({ @@ -1204,7 +1098,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.job.logs.get", { + const result = await runCliCapability(runner, "workflow.job.logs.view", { owner: "acme", name: "modkit", jobId: 300, @@ -1259,11 +1153,6 @@ describe("runCliCapability", () => { name: "modkit", jobId: 0, }) - const checkRunInvalidIdResult = await runCliCapability(runner, "check_run.annotations.list", { - owner: "acme", - name: "modkit", - checkRunId: 0, - }) expect(checksResult.ok).toBe(false) expect(mergeabilityResult.ok).toBe(false) @@ -1271,7 +1160,6 @@ describe("runCliCapability", () => { expect(workflowListResult.ok).toBe(false) expect(workflowLogsResult.ok).toBe(false) - expect(checkRunInvalidIdResult.ok).toBe(false) expect(runner.run).not.toHaveBeenCalled() }) @@ -1365,11 +1253,6 @@ describe("runCliCapability", () => { name: "modkit", first: 1, }) - const annotationsResult = await runCliCapability(runner, "check_run.annotations.list", { - owner: "acme", - name: "modkit", - checkRunId: 1, - }) expect(checksResult.ok).toBe(true) expect(checksResult.data).toEqual( @@ -1378,16 +1261,16 @@ describe("runCliCapability", () => { expect.objectContaining({ name: null, state: null, - bucket: null, workflow: null, link: null, + annotations: [], }), expect.objectContaining({ name: null, state: null, - bucket: null, workflow: null, link: null, + annotations: [], }), ], }), @@ -1397,11 +1280,6 @@ describe("runCliCapability", () => { items: [expect.objectContaining({ id: 0, workflowName: null, status: null })], }), ) - expect(annotationsResult.data).toEqual( - expect.objectContaining({ - items: [expect.objectContaining({ path: null, level: null, message: null })], - }), - ) }) it("normalizes non-array checks and non-object workflow payloads", async () => { @@ -1581,7 +1459,7 @@ describe("runCliCapability", () => { name: "modkit", first: 10, }) - const workflowGet = await runCliCapability(runner, "workflow.get", { + const workflowGet = await runCliCapability(runner, "workflow.view", { owner: "acme", name: "modkit", workflowId: "ci.yml", @@ -1591,7 +1469,7 @@ describe("runCliCapability", () => { name: "modkit", runId: 123, }) - const rerunAll = await runCliCapability(runner, "workflow.run.rerun_all", { + const rerunAll = await runCliCapability(runner, "workflow.run.rerun.all", { owner: "acme", name: "modkit", runId: 123, @@ -1610,7 +1488,7 @@ describe("runCliCapability", () => { expect(workflowList.ok).toBe(true) expect(workflowGet.ok).toBe(true) expect(workflowRunGet.ok).toBe(true) - expect(rerunAll.data).toEqual({ runId: 123, status: "requested" }) + expect(rerunAll.data).toEqual({ runId: 123, queued: true }) expect(cancel.data).toEqual({ runId: 123, status: "cancel_requested" }) expect(artifacts.data).toEqual( expect.objectContaining({ @@ -1656,7 +1534,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.get", { + const result = await runCliCapability(runner, "workflow.view", { owner: "acme", name: "modkit", workflowId: 123, @@ -1686,7 +1564,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.dispatch.run", { + const result = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "release.yml", @@ -1762,11 +1640,11 @@ describe("runCliCapability", () => { .mockResolvedValueOnce({ stdout: "", stderr: "", exitCode: 0 }), } - const org = await runCliCapability(runner, "project_v2.org.get", { + const org = await runCliCapability(runner, "project_v2.org.view", { org: "acme", projectNumber: 1, }) - const user = await runCliCapability(runner, "project_v2.user.get", { + const user = await runCliCapability(runner, "project_v2.user.view", { user: "octocat", projectNumber: 2, }) @@ -1779,12 +1657,12 @@ describe("runCliCapability", () => { projectNumber: 1, first: 10, }) - const addIssue = await runCliCapability(runner, "project_v2.item.add_issue", { + const addIssue = await runCliCapability(runner, "project_v2.items.issue.add", { owner: "acme", projectNumber: 1, issueUrl: "https://github.com/acme/modkit/issues/10", }) - const fieldUpdate = await runCliCapability(runner, "project_v2.item.field.update", { + const fieldUpdate = await runCliCapability(runner, "project_v2.items.field.update", { projectId: "PVT_org_1", itemId: "PVTI_1", fieldId: "PVTF_1", @@ -1992,7 +1870,7 @@ describe("runCliCapability", () => { name: "modkit", first: 10, }) - const getResult = await runCliCapability(runner, "release.get", { + const getResult = await runCliCapability(runner, "release.view", { owner: "acme", name: "modkit", tagName: "v1.0.1", @@ -2112,7 +1990,7 @@ describe("runCliCapability", () => { }), } - const createResult = await runCliCapability(runner, "release.create_draft", { + const createResult = await runCliCapability(runner, "release.create", { owner: "acme", name: "modkit", tagName: "v2.0.0-rc.1", @@ -2130,7 +2008,7 @@ describe("runCliCapability", () => { draft: true, }) - const publishResult = await runCliCapability(runner, "release.publish_draft", { + const publishResult = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 201, @@ -2178,7 +2056,7 @@ describe("runCliCapability", () => { }), } - const dispatchResult = await runCliCapability(runner, "workflow.dispatch.run", { + const dispatchResult = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "release.yml", @@ -2189,13 +2067,13 @@ describe("runCliCapability", () => { }, }) - const rerunResult = await runCliCapability(runner, "workflow.run.rerun_failed", { + const rerunResult = await runCliCapability(runner, "workflow.run.rerun.failed", { owner: "acme", name: "modkit", runId: 500, }) - const invalidDispatchResult = await runCliCapability(runner, "workflow.dispatch.run", { + const invalidDispatchResult = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "release.yml", @@ -2212,14 +2090,14 @@ describe("runCliCapability", () => { expect(rerunResult.ok).toBe(true) expect(rerunResult.data).toEqual({ runId: 500, - rerunFailed: true, + queued: true, }) expect(invalidDispatchResult.ok).toBe(false) expect(invalidDispatchResult.error?.code).toBe("VALIDATION") }) - it("maps release.publish_draft command failure after draft pre-check", async () => { + it("maps release.publish command failure after draft pre-check", async () => { const runner = { run: vi .fn() @@ -2245,7 +2123,7 @@ describe("runCliCapability", () => { }), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 301, @@ -2256,13 +2134,13 @@ describe("runCliCapability", () => { expect(result.error?.code).toBe("AUTH") expect(result.error?.details).toEqual( expect.objectContaining({ - capabilityId: "release.publish_draft", + capabilityId: "release.publish", exitCode: 1, }), ) }) - it("rejects release.publish_draft when current release is not draft", async () => { + it("rejects release.publish when current release is not draft", async () => { const runner = { run: vi.fn(async () => ({ stdout: JSON.stringify({ @@ -2281,7 +2159,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 401, @@ -2294,7 +2172,7 @@ describe("runCliCapability", () => { expect(runner.run).toHaveBeenCalledTimes(1) }) - it("maps release.publish_draft pre-check read failure", async () => { + it("maps release.publish pre-check read failure", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -2303,7 +2181,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 500, @@ -2314,14 +2192,14 @@ describe("runCliCapability", () => { expect(result.error?.code).toBe("AUTH") expect(result.error?.details).toEqual( expect.objectContaining({ - capabilityId: "release.publish_draft", + capabilityId: "release.publish", exitCode: 1, }), ) expect(runner.run).toHaveBeenCalledTimes(1) }) - it("rejects release.publish_draft when pre-check payload is not an object", async () => { + it("rejects release.publish when pre-check payload is not an object", async () => { const runner = { run: vi.fn(async () => ({ stdout: JSON.stringify([]), @@ -2330,7 +2208,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 501, @@ -2381,7 +2259,7 @@ describe("runCliCapability", () => { expect(result.data).toEqual({ prNumber: 42, updated: true }) }) - it("returns validation error when release.publish_draft params are incomplete", async () => { + it("returns validation error when release.publish params are incomplete", async () => { const runner = { run: vi.fn(async () => ({ stdout: "{}", @@ -2390,7 +2268,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", }) @@ -2446,12 +2324,12 @@ describe("runCliCapability", () => { projectNumber: 1, first: 10, }) - const addIssueResult = await runCliCapability(runner, "project_v2.item.add_issue", { + const addIssueResult = await runCliCapability(runner, "project_v2.items.issue.add", { owner: "acme", projectNumber: 1, issueUrl: "https://github.com/acme/modkit/issues/1", }) - const releaseGetResult = await runCliCapability(runner, "release.get", { + const releaseGetResult = await runCliCapability(runner, "release.view", { owner: "acme", name: "modkit", tagName: "v1.0.0", @@ -2574,28 +2452,28 @@ describe("runCliCapability", () => { message: "deleteBranch", }, { - capabilityId: "pr.checks.rerun_failed", + capabilityId: "pr.checks.rerun.failed", params: { prNumber: 0, runId: 1 }, message: "prNumber", }, { - capabilityId: "pr.review.request", + capabilityId: "pr.reviews.request", params: { prNumber: 0, reviewers: ["octocat"] }, message: "prNumber", }, { - capabilityId: "pr.review.request", + capabilityId: "pr.reviews.request", params: { prNumber: 1, reviewers: [] }, message: "reviewers", }, { - capabilityId: "pr.assignees.update", - params: { prNumber: 0, add: ["octocat"] }, + capabilityId: "pr.assignees.add", + params: { prNumber: 0, assignees: ["octocat"] }, message: "prNumber", }, { - capabilityId: "pr.assignees.update", - params: { prNumber: 1, add: [], remove: [] }, + capabilityId: "pr.assignees.add", + params: { prNumber: 1, assignees: [] }, message: "assignees", }, { capabilityId: "pr.branch.update", params: { prNumber: 0 }, message: "prNumber" }, @@ -2606,7 +2484,7 @@ describe("runCliCapability", () => { message: "first", }, { - capabilityId: "workflow.get", + capabilityId: "workflow.view", params: { owner: "acme", name: "modkit", workflowId: null }, message: "workflowId", }, @@ -2616,7 +2494,7 @@ describe("runCliCapability", () => { message: "runId", }, { - capabilityId: "workflow.run.rerun_all", + capabilityId: "workflow.run.rerun.all", params: { owner: "acme", name: "modkit", runId: 0 }, message: "runId", }, @@ -2641,12 +2519,12 @@ describe("runCliCapability", () => { message: "first", }, { - capabilityId: "release.get", + capabilityId: "release.view", params: { owner: "acme", name: "modkit", tagName: "" }, message: "tagName", }, { - capabilityId: "release.create_draft", + capabilityId: "release.create", params: { owner: "acme", name: "modkit", tagName: "" }, message: "tagName", }, @@ -2656,27 +2534,27 @@ describe("runCliCapability", () => { message: "releaseId", }, { - capabilityId: "release.publish_draft", + capabilityId: "release.publish", params: { owner: "acme", name: "modkit", releaseId: 0 }, message: "releaseId", }, { - capabilityId: "workflow.dispatch.run", + capabilityId: "workflow.dispatch", params: { owner: "acme", name: "modkit", ref: "main" }, message: "workflowId", }, { - capabilityId: "workflow.dispatch.run", + capabilityId: "workflow.dispatch", params: { owner: "acme", name: "modkit", workflowId: "ci" }, message: "ref", }, { - capabilityId: "workflow.dispatch.run", + capabilityId: "workflow.dispatch", params: { owner: "acme", name: "modkit", workflowId: "ci", ref: "main", inputs: null }, message: "inputs", }, { - capabilityId: "workflow.dispatch.run", + capabilityId: "workflow.dispatch", params: { owner: "acme", name: "modkit", @@ -2687,7 +2565,7 @@ describe("runCliCapability", () => { message: "inputs", }, { - capabilityId: "workflow.dispatch.run", + capabilityId: "workflow.dispatch", params: { owner: "acme", name: "modkit", @@ -2698,17 +2576,17 @@ describe("runCliCapability", () => { message: "inputs", }, { - capabilityId: "workflow.run.rerun_failed", + capabilityId: "workflow.run.rerun.failed", params: { owner: "acme", name: "modkit", runId: 0 }, message: "runId", }, { - capabilityId: "project_v2.item.add_issue", + capabilityId: "project_v2.items.issue.add", params: { owner: "acme", projectNumber: 1, issueUrl: "" }, message: "owner/projectNumber/issueUrl", }, { - capabilityId: "project_v2.item.field.update", + capabilityId: "project_v2.items.field.update", params: { projectId: "", itemId: "", fieldId: "" }, message: "projectId/itemId/fieldId", }, @@ -2744,7 +2622,7 @@ describe("runCliCapability", () => { }), } - const fieldUpdateResult = await runCliCapability(runner, "project_v2.item.field.update", { + const fieldUpdateResult = await runCliCapability(runner, "project_v2.items.field.update", { projectId: "project-1", itemId: "item-42", fieldId: "field-9", @@ -2818,19 +2696,18 @@ describe("runCliCapability", () => { .mockResolvedValueOnce({ stdout: "updated assignees", stderr: "", exitCode: 0 }), } - const reviewersResult = await runCliCapability(runner, "pr.review.request", { + const reviewersResult = await runCliCapability(runner, "pr.reviews.request", { owner: "acme", name: "modkit", prNumber: 42, reviewers: ["octocat"], }) - const assigneesResult = await runCliCapability(runner, "pr.assignees.update", { + const assigneesResult = await runCliCapability(runner, "pr.assignees.add", { owner: "acme", name: "modkit", prNumber: 42, - add: ["octocat"], - remove: ["hubot"], + assignees: ["octocat"], }) expect(reviewersResult.ok).toBe(true) @@ -2841,12 +2718,12 @@ describe("runCliCapability", () => { }) expect(assigneesResult.ok).toBe(true) - expect(assigneesResult.data).toEqual({ - prNumber: 42, - add: ["octocat"], - remove: ["hubot"], - updated: true, - }) + expect(assigneesResult.data).toEqual( + expect.objectContaining({ + prNumber: 42, + added: ["octocat"], + }), + ) }) it("surfaces rerun-all failure details when fallback rerun also fails", async () => { @@ -2866,7 +2743,7 @@ describe("runCliCapability", () => { }), } - const result = await runCliCapability(runner, "pr.checks.rerun_failed", { + const result = await runCliCapability(runner, "pr.checks.rerun.failed", { owner: "acme", name: "modkit", prNumber: 10, @@ -2919,7 +2796,7 @@ describe("runCliCapability", () => { name: "modkit", first: 5, }) - const workflowGetResult = await runCliCapability(runner, "workflow.get", { + const workflowGetResult = await runCliCapability(runner, "workflow.view", { owner: "acme", name: "modkit", workflowId: "ci.yml", @@ -2934,7 +2811,7 @@ describe("runCliCapability", () => { name: "modkit", runId: 123, }) - const projectOrgResult = await runCliCapability(runner, "project_v2.org.get", { + const projectOrgResult = await runCliCapability(runner, "project_v2.org.view", { org: "acme", projectNumber: 1, }) @@ -3626,7 +3503,7 @@ describe("runCliCapability", () => { expect(call[1]).toContain("Updated description") }) - it("handles release.publish_draft when draft read check fails", async () => { + it("handles release.publish when draft read check fails", async () => { const runner = { run: vi.fn().mockResolvedValueOnce({ stdout: "", @@ -3635,7 +3512,7 @@ describe("runCliCapability", () => { }), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 999, @@ -3646,7 +3523,7 @@ describe("runCliCapability", () => { expect(runner.run).toHaveBeenCalledTimes(1) }) - it("handles release.publish_draft when publish command fails after draft check passes", async () => { + it("handles release.publish when publish command fails after draft check passes", async () => { const runner = { run: vi .fn() @@ -3662,7 +3539,7 @@ describe("runCliCapability", () => { }), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 123, @@ -3673,7 +3550,7 @@ describe("runCliCapability", () => { expect(runner.run).toHaveBeenCalledTimes(2) }) - it("handles release.publish_draft with non-object draft response", async () => { + it("handles release.publish with non-object draft response", async () => { const runner = { run: vi.fn().mockResolvedValueOnce({ stdout: "invalid response", @@ -3682,7 +3559,7 @@ describe("runCliCapability", () => { }), } - const result = await runCliCapability(runner, "release.publish_draft", { + const result = await runCliCapability(runner, "release.publish", { owner: "acme", name: "modkit", releaseId: 123, @@ -3864,7 +3741,7 @@ describe("runCliCapability", () => { expect(result.error?.message).toContain("head") }) - it("handles workflow.dispatch.run with empty inputs object", async () => { + it("handles workflow.dispatch with empty inputs object", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -3873,7 +3750,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.dispatch.run", { + const result = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "ci.yml", @@ -3886,7 +3763,7 @@ describe("runCliCapability", () => { expect(call[1].join("/")).toContain("workflows/ci.yml/dispatches") }) - it("handles workflow.dispatch.run with multiple input types", async () => { + it("handles workflow.dispatch with multiple input types", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -3895,7 +3772,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.dispatch.run", { + const result = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "ci.yml", @@ -3914,7 +3791,7 @@ describe("runCliCapability", () => { expect(call[1].join(" ")).toContain("inputs[name]=test") }) - it("handles workflow.dispatch.run with invalid input value", async () => { + it("handles workflow.dispatch with invalid input value", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -3923,7 +3800,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.dispatch.run", { + const result = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "ci.yml", @@ -3938,7 +3815,7 @@ describe("runCliCapability", () => { expect(result.error?.message).toContain("inputs") }) - it("handles workflow.dispatch.run with empty input key", async () => { + it("handles workflow.dispatch with empty input key", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -3947,7 +3824,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.dispatch.run", { + const result = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "ci.yml", @@ -3961,7 +3838,7 @@ describe("runCliCapability", () => { expect(result.error?.code).toBe("VALIDATION") }) - it("handles workflow.dispatch.run with invalid inputs type", async () => { + it("handles workflow.dispatch with invalid inputs type", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -3970,7 +3847,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.dispatch.run", { + const result = await runCliCapability(runner, "workflow.dispatch", { owner: "acme", name: "modkit", workflowId: "ci.yml", @@ -3982,7 +3859,7 @@ describe("runCliCapability", () => { expect(result.error?.code).toBe("VALIDATION") }) - it("handles project_v2.item.field.update with valueNumber (finite check)", async () => { + it("handles project_v2.items.field.update with valueNumber (finite check)", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -3991,7 +3868,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "project_v2.item.field.update", { + const result = await runCliCapability(runner, "project_v2.items.field.update", { projectId: "PVT_kwDO123", itemId: "ITEM_123", fieldId: "FIELD_123", @@ -4004,7 +3881,7 @@ describe("runCliCapability", () => { expect(call[1]).toContain("42") }) - it("handles project_v2.item.field.update with clear flag", async () => { + it("handles project_v2.items.field.update with clear flag", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -4013,7 +3890,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "project_v2.item.field.update", { + const result = await runCliCapability(runner, "project_v2.items.field.update", { projectId: "PVT_kwDO123", itemId: "ITEM_123", fieldId: "FIELD_123", @@ -4025,7 +3902,7 @@ describe("runCliCapability", () => { expect(call[1]).toContain("--clear") }) - it("handles project_v2.item.field.update without any value", async () => { + it("handles project_v2.items.field.update without any value", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -4034,7 +3911,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "project_v2.item.field.update", { + const result = await runCliCapability(runner, "project_v2.items.field.update", { projectId: "PVT_kwDO123", itemId: "ITEM_123", fieldId: "FIELD_123", @@ -4045,7 +3922,7 @@ describe("runCliCapability", () => { expect(result.error?.message).toContain("Missing field value update") }) - it("handles project_v2.item.field.update with Infinity value", async () => { + it("handles project_v2.items.field.update with Infinity value", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -4054,7 +3931,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "project_v2.item.field.update", { + const result = await runCliCapability(runner, "project_v2.items.field.update", { projectId: "PVT_kwDO123", itemId: "ITEM_123", fieldId: "FIELD_123", @@ -4065,7 +3942,7 @@ describe("runCliCapability", () => { expect(result.error?.code).toBe("UNKNOWN") }) - it("handles pr.review.request with whitespace-only reviewers filtered out", async () => { + it("handles pr.reviews.request with whitespace-only reviewers filtered out", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -4074,7 +3951,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "pr.review.request", { + const result = await runCliCapability(runner, "pr.reviews.request", { owner: "acme", name: "modkit", prNumber: 10, @@ -4092,7 +3969,7 @@ describe("runCliCapability", () => { expect(call[1]).toContain("alice,bob") }) - it("handles pr.assignees.update with add and remove lists", async () => { + it("handles pr.assignees.add", async () => { const runner = { run: vi.fn(async () => ({ stdout: "", @@ -4101,29 +3978,26 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "pr.assignees.update", { + const result = await runCliCapability(runner, "pr.assignees.add", { owner: "acme", name: "modkit", prNumber: 10, - add: ["alice"], - remove: ["bob"], + assignees: ["alice"], }) expect(result.ok).toBe(true) - expect(result.data).toEqual({ - prNumber: 10, - add: ["alice"], - remove: ["bob"], - updated: true, - }) + expect(result.data).toEqual( + expect.objectContaining({ + prNumber: 10, + added: ["alice"], + }), + ) const call = runner.run.mock.calls[0] as unknown as [string, string[], number] expect(call[1]).toContain("--add-assignee") expect(call[1]).toContain("alice") - expect(call[1]).toContain("--remove-assignee") - expect(call[1]).toContain("bob") }) - it("handles workflow.job.logs.get with zero error/warning lines", async () => { + it("handles workflow.job.logs.view with zero error/warning lines", async () => { const runner = { run: vi.fn(async () => ({ stdout: "Build started\nCompilation successful\nTests passed", @@ -4132,7 +4006,7 @@ describe("runCliCapability", () => { })), } - const result = await runCliCapability(runner, "workflow.job.logs.get", { + const result = await runCliCapability(runner, "workflow.job.logs.view", { owner: "acme", name: "modkit", jobId: 123, @@ -4150,7 +4024,7 @@ describe("runCliCapability", () => { }) }) - it("handles workflow.job.logs.get with multiple error and warning lines", async () => { + it("handles workflow.job.logs.view with multiple error and warning lines", async () => { const runner = { run: vi.fn(async () => ({ stdout: `Error: File not found @@ -4164,7 +4038,7 @@ ERROR: Critical issue`, })), } - const result = await runCliCapability(runner, "workflow.job.logs.get", { + const result = await runCliCapability(runner, "workflow.job.logs.view", { owner: "acme", name: "modkit", jobId: 123, @@ -4259,7 +4133,7 @@ ERROR: Critical issue`, })), } - const result = await runCliCapability(runner, "workflow.get", { + const result = await runCliCapability(runner, "workflow.view", { owner: "acme", name: "modkit", workflowId: 456, @@ -4327,37 +4201,6 @@ ERROR: Critical issue`, expect(call[1]).toContain("30") }) - it("handles check_run.annotations.list with partial annotation fields", async () => { - const runner = { - run: vi.fn(async () => ({ - stdout: JSON.stringify([ - { - path: "src/main.ts", - }, - ]), - stderr: "", - exitCode: 0, - })), - } - - const result = await runCliCapability(runner, "check_run.annotations.list", { - owner: "acme", - name: "modkit", - checkRunId: 123, - }) - - expect(result.ok).toBe(true) - expect((result.data as Record).items?.[0]).toEqual({ - path: "src/main.ts", - startLine: null, - endLine: null, - level: null, - message: null, - title: null, - details: null, - }) - }) - it("normalizes workflow.run.view with jobs array containing various states", async () => { const runner = { run: vi.fn(async () => ({ diff --git a/packages/core/test/unit/cli-capability-registry.test.ts b/packages/core/test/unit/cli-capability-registry.test.ts index 0fd83988..7440e18c 100644 --- a/packages/core/test/unit/cli-capability-registry.test.ts +++ b/packages/core/test/unit/cli-capability-registry.test.ts @@ -13,49 +13,53 @@ const ALL_CLI_CAPABILITY_IDS = [ "issue.view", "issue.list", "issue.comments.list", + "issue.labels.remove", + "issue.assignees.add", + "issue.assignees.remove", + "issue.milestone.clear", // pr "pr.view", "pr.list", "pr.create", "pr.update", "pr.checks.list", - "pr.checks.failed", + "pr.checks.rerun.failed", + "pr.checks.rerun.all", "pr.merge.status", - "pr.review.submit", + "pr.reviews.submit", "pr.merge", - "pr.checks.rerun_failed", - "pr.checks.rerun_all", - "pr.review.request", - "pr.assignees.update", + "pr.reviews.request", + "pr.assignees.add", + "pr.assignees.remove", "pr.branch.update", "pr.diff.view", "pr.diff.files", - "check_run.annotations.list", // workflow "workflow.runs.list", "workflow.job.logs.raw", - "workflow.job.logs.get", + "workflow.job.logs.view", "workflow.list", - "workflow.get", + "workflow.view", "workflow.run.view", - "workflow.run.rerun_all", + "workflow.run.rerun.all", "workflow.run.cancel", "workflow.run.artifacts.list", - "workflow.dispatch.run", - "workflow.run.rerun_failed", + "workflow.dispatch", + "workflow.run.rerun.failed", // project-v2 - "project_v2.org.get", - "project_v2.user.get", + "project_v2.org.view", + "project_v2.user.view", "project_v2.fields.list", "project_v2.items.list", - "project_v2.item.add_issue", - "project_v2.item.field.update", + "project_v2.items.issue.add", + "project_v2.items.issue.remove", + "project_v2.items.field.update", // release "release.list", - "release.get", - "release.create_draft", + "release.view", + "release.create", "release.update", - "release.publish_draft", + "release.publish", ] as const describe("getCliHandler", () => { diff --git a/packages/core/test/unit/cli-domains-issue.test.ts b/packages/core/test/unit/cli-domains-issue.test.ts index e2c36834..b95d0959 100644 --- a/packages/core/test/unit/cli-domains-issue.test.ts +++ b/packages/core/test/unit/cli-domains-issue.test.ts @@ -1,6 +1,10 @@ import { + handleIssueAssigneesAdd, + handleIssueAssigneesRemove, handleIssueCommentsList, + handleIssueLabelsRemove, handleIssueList, + handleIssueMilestoneClear, handleIssueView, } from "@core/core/execution/adapters/cli/domains/issue.js" import type { CliCommandRunner } from "@core/core/execution/adapters/cli-adapter.js" @@ -406,3 +410,225 @@ describe("issue domain handlers – additional coverage", () => { }) }) }) + +describe("issue domain handlers – new capabilities", () => { + describe("handleIssueLabelsRemove", () => { + it("returns success with removed labels", async () => { + const result = await handleIssueLabelsRemove( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 42, labels: ["bug", "wontfix"] }, + undefined, + ) + expect(result.ok).toBe(true) + expect(result.data).toMatchObject({ issueNumber: 42, removed: ["bug", "wontfix"] }) + expect(result.meta.capability_id).toBe("issue.labels.remove") + expect(result.meta.route_used).toBe("cli") + }) + + it("returns error for invalid issueNumber", async () => { + const result = await handleIssueLabelsRemove( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 0, labels: ["bug"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("issueNumber") + }) + + it("returns error for empty labels array", async () => { + const result = await handleIssueLabelsRemove( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 1, labels: [] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("labels") + }) + + it("returns error on non-zero exit code", async () => { + const result = await handleIssueLabelsRemove( + mockRunner(1, "", "label not found"), + { owner: "owner", name: "repo", issueNumber: 1, labels: ["missing"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.code).toBeDefined() + }) + + it("returns error when runner throws", async () => { + const runner = { + run: vi.fn().mockRejectedValue(new Error("timeout")), + } as unknown as CliCommandRunner + + const result = await handleIssueLabelsRemove( + runner, + { owner: "owner", name: "repo", issueNumber: 1, labels: ["bug"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("timeout") + }) + }) + + describe("handleIssueAssigneesAdd", () => { + it("returns success with added assignees", async () => { + const result = await handleIssueAssigneesAdd( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 7, assignees: ["alice", "bob"] }, + undefined, + ) + expect(result.ok).toBe(true) + expect(result.data).toMatchObject({ issueNumber: 7, added: ["alice", "bob"] }) + expect(result.meta.capability_id).toBe("issue.assignees.add") + }) + + it("returns error for invalid issueNumber", async () => { + const result = await handleIssueAssigneesAdd( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: -1, assignees: ["alice"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("issueNumber") + }) + + it("returns error for empty assignees array", async () => { + const result = await handleIssueAssigneesAdd( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 1, assignees: [] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("assignees") + }) + + it("returns error on non-zero exit code", async () => { + const result = await handleIssueAssigneesAdd( + mockRunner(1, "", "user not found"), + { owner: "owner", name: "repo", issueNumber: 1, assignees: ["ghost"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.code).toBeDefined() + }) + + it("returns error when runner throws", async () => { + const runner = { + run: vi.fn().mockRejectedValue(new Error("network error")), + } as unknown as CliCommandRunner + + const result = await handleIssueAssigneesAdd( + runner, + { owner: "owner", name: "repo", issueNumber: 1, assignees: ["alice"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("network error") + }) + }) + + describe("handleIssueAssigneesRemove", () => { + it("returns success with removed assignees", async () => { + const result = await handleIssueAssigneesRemove( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 5, assignees: ["carol"] }, + undefined, + ) + expect(result.ok).toBe(true) + expect(result.data).toMatchObject({ issueNumber: 5, removed: ["carol"] }) + expect(result.meta.capability_id).toBe("issue.assignees.remove") + }) + + it("returns error for invalid issueNumber", async () => { + const result = await handleIssueAssigneesRemove( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 0, assignees: ["carol"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("issueNumber") + }) + + it("returns error for empty assignees array", async () => { + const result = await handleIssueAssigneesRemove( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 1, assignees: [] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("assignees") + }) + + it("returns error on non-zero exit code", async () => { + const result = await handleIssueAssigneesRemove( + mockRunner(1, "", "permission denied"), + { owner: "owner", name: "repo", issueNumber: 1, assignees: ["carol"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.code).toBeDefined() + }) + + it("returns error when runner throws", async () => { + const runner = { + run: vi.fn().mockRejectedValue(new Error("runner failure")), + } as unknown as CliCommandRunner + + const result = await handleIssueAssigneesRemove( + runner, + { owner: "owner", name: "repo", issueNumber: 1, assignees: ["carol"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("runner failure") + }) + }) + + describe("handleIssueMilestoneClear", () => { + it("returns success with cleared: true", async () => { + const result = await handleIssueMilestoneClear( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 3 }, + undefined, + ) + expect(result.ok).toBe(true) + expect(result.data).toMatchObject({ issueNumber: 3, cleared: true }) + expect(result.meta.capability_id).toBe("issue.milestone.clear") + expect(result.meta.route_used).toBe("cli") + }) + + it("returns error for invalid issueNumber", async () => { + const result = await handleIssueMilestoneClear( + mockRunner(0, ""), + { owner: "owner", name: "repo", issueNumber: 0 }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("issueNumber") + }) + + it("returns error on non-zero exit code", async () => { + const result = await handleIssueMilestoneClear( + mockRunner(1, "", "issue not found"), + { owner: "owner", name: "repo", issueNumber: 99 }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.code).toBeDefined() + }) + + it("returns error when runner throws", async () => { + const runner = { + run: vi.fn().mockRejectedValue(new Error("timeout")), + } as unknown as CliCommandRunner + + const result = await handleIssueMilestoneClear( + runner, + { owner: "owner", name: "repo", issueNumber: 1 }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("timeout") + }) + }) +}) diff --git a/packages/core/test/unit/cli-domains-pr.test.ts b/packages/core/test/unit/cli-domains-pr.test.ts index e62bd328..ffa7d201 100644 --- a/packages/core/test/unit/cli-domains-pr.test.ts +++ b/packages/core/test/unit/cli-domains-pr.test.ts @@ -272,66 +272,20 @@ describe("pr domain handlers", () => { expect(result.ok).toBe(true) expect(result.data).toMatchObject({ items: [ - { name: "test", state: "PASS", bucket: "PASS" }, - { name: "lint", state: "FAIL", bucket: "FAIL" }, - { name: "build", state: "PENDING", bucket: "PENDING" }, + { name: "test", state: "PASS" }, + { name: "lint", state: "FAIL" }, + { name: "build", state: "PENDING" }, ], summary: { total: 3, failed: 1, pending: 1, passed: 1 }, }) }) }) - describe("pr.checks.failed", () => { - it("returns only failed checks", async () => { - const runner = mockRunner( - 0, - JSON.stringify([ - { - name: "test", - state: "PASS", - bucket: "PASS", - workflow: "test.yml", - link: "https://...", - }, - { - name: "lint", - state: "FAIL", - bucket: "FAIL", - workflow: "lint.yml", - link: "https://...", - }, - { - name: "build", - state: "FAIL", - bucket: "FAIL", - workflow: "build.yml", - link: "https://...", - }, - ]), - ) - - const result = await h("pr.checks.failed")( - runner, - { owner: "owner", name: "repo", prNumber: 123 }, - undefined, - ) - - expect(result.ok).toBe(true) - expect(result.data).toMatchObject({ - items: [ - { name: "lint", state: "FAIL", bucket: "FAIL" }, - { name: "build", state: "FAIL", bucket: "FAIL" }, - ], - summary: { total: 3, failed: 2, pending: 0, passed: 1 }, - }) - }) - }) - - describe("pr.checks.rerun_failed", () => { - it("succeeds with mode failed", async () => { + describe("pr.checks.rerun.failed", () => { + it("succeeds with runId integer and queued", async () => { const runner = mockRunner(0, "", "") - const result = await h("pr.checks.rerun_failed")( + const result = await h("pr.checks.rerun.failed")( runner, { owner: "owner", name: "repo", prNumber: 123, runId: 999 }, undefined, @@ -339,9 +293,7 @@ describe("pr domain handlers", () => { expect(result.ok).toBe(true) expect(result.data).toMatchObject({ - prNumber: 123, runId: 999, - mode: "failed", queued: true, }) }) @@ -358,7 +310,7 @@ describe("pr domain handlers", () => { const runner = { run: runSpy } as unknown as CliCommandRunner - const result = await h("pr.checks.rerun_failed")( + const result = await h("pr.checks.rerun.failed")( runner, { owner: "owner", name: "repo", prNumber: 123, runId: 999 }, undefined, @@ -366,9 +318,7 @@ describe("pr domain handlers", () => { expect(result.ok).toBe(true) expect(result.data).toMatchObject({ - prNumber: 123, runId: 999, - mode: "all", queued: true, }) expect(runSpy).toHaveBeenCalledTimes(2) @@ -377,7 +327,7 @@ describe("pr domain handlers", () => { it("returns error on non-zero exit code", async () => { const runner = mockRunner(1, "", "run not found") - const result = await h("pr.checks.rerun_failed")( + const result = await h("pr.checks.rerun.failed")( runner, { owner: "owner", name: "repo", prNumber: 123, runId: 999 }, undefined, @@ -388,11 +338,11 @@ describe("pr domain handlers", () => { }) }) - describe("pr.checks.rerun_all", () => { - it("succeeds with mode all", async () => { + describe("pr.checks.rerun.all", () => { + it("succeeds with runId integer and queued", async () => { const runner = mockRunner(0, "", "") - const result = await h("pr.checks.rerun_all")( + const result = await h("pr.checks.rerun.all")( runner, { owner: "owner", name: "repo", prNumber: 123, runId: 999 }, undefined, @@ -400,20 +350,18 @@ describe("pr domain handlers", () => { expect(result.ok).toBe(true) expect(result.data).toMatchObject({ - prNumber: 123, runId: 999, - mode: "all", queued: true, }) }) }) - describe("pr.review.submit", () => { + describe("pr.reviews.submit", () => { it("APPROVE with optional body", async () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - const result = await h("pr.review.submit")( + const result = await h("pr.reviews.submit")( runner, { owner: "owner", @@ -443,7 +391,7 @@ describe("pr domain handlers", () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("pr.review.submit")( + await h("pr.reviews.submit")( runner, { owner: "owner", @@ -473,7 +421,7 @@ describe("pr domain handlers", () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("pr.review.submit")( + await h("pr.reviews.submit")( runner, { owner: "owner", @@ -546,12 +494,12 @@ describe("pr domain handlers", () => { }) }) - describe("pr.review.request", () => { + describe("pr.reviews.request", () => { it("succeeds with reviewers list", async () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - const result = await h("pr.review.request")( + const result = await h("pr.reviews.request")( runner, { owner: "owner", @@ -576,19 +524,18 @@ describe("pr domain handlers", () => { }) }) - describe("pr.assignees.update", () => { - it("adds and removes assignees", async () => { + describe("pr.assignees.add", () => { + it("adds assignees", async () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - const result = await h("pr.assignees.update")( + const result = await h("pr.assignees.add")( runner, { owner: "owner", name: "repo", prNumber: 123, - add: ["alice"], - remove: ["bob"], + assignees: ["alice", "bob"], }, undefined, ) @@ -596,21 +543,40 @@ describe("pr domain handlers", () => { expect(result.ok).toBe(true) expect(result.data).toMatchObject({ prNumber: 123, - add: ["alice"], - remove: ["bob"], - updated: true, + added: ["alice", "bob"], }) expect(runSpy).toHaveBeenCalledWith( "gh", - expect.arrayContaining([ - "pr", - "edit", - "123", - "--add-assignee", - "alice", - "--remove-assignee", - "bob", - ]), + expect.arrayContaining(["pr", "edit", "123", "--add-assignee", "alice,bob"]), + expect.any(Number), + ) + }) + }) + + describe("pr.assignees.remove", () => { + it("removes assignees", async () => { + const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) + const runner = { run: runSpy } as unknown as CliCommandRunner + + const result = await h("pr.assignees.remove")( + runner, + { + owner: "owner", + name: "repo", + prNumber: 123, + assignees: ["alice", "bob"], + }, + undefined, + ) + + expect(result.ok).toBe(true) + expect(result.data).toMatchObject({ + prNumber: 123, + removed: ["alice", "bob"], + }) + expect(runSpy).toHaveBeenCalledWith( + "gh", + expect.arrayContaining(["pr", "edit", "123", "--remove-assignee", "alice,bob"]), expect.any(Number), ) }) @@ -686,78 +652,6 @@ describe("pr domain handlers", () => { }) }) - describe("check_run.annotations.list", () => { - it("returns annotations with mapped fields", async () => { - const runner = mockRunner( - 0, - JSON.stringify([ - { - path: "src/main.ts", - start_line: 10, - end_line: 15, - annotation_level: "warning", - message: "Unused variable", - title: "Lint warning", - raw_details: "Details here", - }, - { - path: "src/utils.ts", - start_line: 20, - end_line: 20, - annotation_level: "error", - message: "Syntax error", - title: "Parse error", - raw_details: null, - }, - ]), - ) - - const result = await h("check_run.annotations.list")( - runner, - { owner: "owner", name: "repo", checkRunId: 555 }, - undefined, - ) - - expect(result.ok).toBe(true) - expect(result.data).toMatchObject({ - items: [ - { - path: "src/main.ts", - startLine: 10, - endLine: 15, - level: "warning", - message: "Unused variable", - title: "Lint warning", - details: "Details here", - }, - { - path: "src/utils.ts", - startLine: 20, - endLine: 20, - level: "error", - message: "Syntax error", - title: "Parse error", - details: null, - }, - ], - }) - }) - - it("returns error on non-zero exit code", async () => { - const runner = mockRunner(1, "", "check run not found") - - const result = await h("check_run.annotations.list")( - runner, - { owner: "owner", name: "repo", checkRunId: 999 }, - undefined, - ) - - expect(result.ok).toBe(false) - expect(result.error?.code).toBeDefined() - expect(result.meta.capability_id).toBe("check_run.annotations.list") - }) - }) - describe("pr.merge.status", () => { it("returns merge status fields", async () => { const runner = mockRunner( @@ -1064,37 +958,6 @@ describe("pr domain handlers – additional coverage", () => { }) }) - describe("pr.checks.failed", () => { - it("returns error for missing prNumber", async () => { - const result = await h("pr.checks.failed")( - mockRunner(0, "[]"), - { owner: "o", name: "r" }, - undefined, - ) - expect(result.ok).toBe(false) - }) - - it("returns error on non-zero exit code", async () => { - const result = await h("pr.checks.failed")( - mockRunner(1, "", "error"), - { owner: "o", name: "r", prNumber: 1 }, - undefined, - ) - expect(result.ok).toBe(false) - expect(result.error?.code).toBeDefined() - }) - - it("returns error on malformed JSON", async () => { - const result = await h("pr.checks.failed")( - mockRunner(0, "not-json"), - { owner: "o", name: "r", prNumber: 1 }, - undefined, - ) - expect(result.ok).toBe(false) - expect(result.error?.message).toContain("Failed to parse CLI JSON output") - }) - }) - describe("pr.merge.status", () => { it("returns error for missing prNumber", async () => { const result = await h("pr.merge.status")( @@ -1126,9 +989,9 @@ describe("pr domain handlers – additional coverage", () => { }) }) - describe("pr.review.submit", () => { + describe("pr.reviews.submit", () => { it("returns error for missing prNumber", async () => { - const result = await h("pr.review.submit")( + const result = await h("pr.reviews.submit")( mockRunner(0, ""), { owner: "o", name: "r", event: "APPROVE" }, undefined, @@ -1138,7 +1001,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error for invalid event", async () => { - const result = await h("pr.review.submit")( + const result = await h("pr.reviews.submit")( mockRunner(0, ""), { owner: "o", name: "r", prNumber: 1, event: "INVALID" }, undefined, @@ -1148,7 +1011,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error for REQUEST_CHANGES without body", async () => { - const result = await h("pr.review.submit")( + const result = await h("pr.reviews.submit")( mockRunner(0, ""), { owner: "o", name: "r", prNumber: 1, event: "REQUEST_CHANGES" }, undefined, @@ -1158,7 +1021,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error for COMMENT without body", async () => { - const result = await h("pr.review.submit")( + const result = await h("pr.reviews.submit")( mockRunner(0, ""), { owner: "o", name: "r", prNumber: 1, event: "COMMENT" }, undefined, @@ -1170,7 +1033,7 @@ describe("pr domain handlers – additional coverage", () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("pr.review.submit")( + await h("pr.reviews.submit")( runner, { owner: "o", name: "r", prNumber: 1, event: "APPROVE" }, undefined, @@ -1182,7 +1045,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error on non-zero exit code", async () => { - const result = await h("pr.review.submit")( + const result = await h("pr.reviews.submit")( mockRunner(1, "", "review failed"), { owner: "o", name: "r", prNumber: 1, event: "APPROVE" }, undefined, @@ -1249,9 +1112,9 @@ describe("pr domain handlers – additional coverage", () => { }) }) - describe("pr.checks.rerun_failed", () => { + describe("pr.checks.rerun.failed", () => { it("returns error for missing prNumber", async () => { - const result = await h("pr.checks.rerun_failed")( + const result = await h("pr.checks.rerun.failed")( mockRunner(0, ""), { owner: "o", name: "r", runId: 999 }, undefined, @@ -1261,7 +1124,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error for missing runId", async () => { - const result = await h("pr.checks.rerun_failed")( + const result = await h("pr.checks.rerun.failed")( mockRunner(0, ""), { owner: "o", name: "r", prNumber: 1 }, undefined, @@ -1281,7 +1144,7 @@ describe("pr domain handlers – additional coverage", () => { .mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "rerun all failed" }) const runner = { run: runSpy } as unknown as CliCommandRunner - const result = await h("pr.checks.rerun_failed")( + const result = await h("pr.checks.rerun.failed")( runner, { owner: "o", name: "r", prNumber: 1, runId: 999 }, undefined, @@ -1292,9 +1155,9 @@ describe("pr domain handlers – additional coverage", () => { }) }) - describe("pr.checks.rerun_all", () => { + describe("pr.checks.rerun.all", () => { it("returns error for missing prNumber", async () => { - const result = await h("pr.checks.rerun_all")( + const result = await h("pr.checks.rerun.all")( mockRunner(0, ""), { owner: "o", name: "r", runId: 999 }, undefined, @@ -1304,7 +1167,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error for missing runId", async () => { - const result = await h("pr.checks.rerun_all")( + const result = await h("pr.checks.rerun.all")( mockRunner(0, ""), { owner: "o", name: "r", prNumber: 1 }, undefined, @@ -1314,7 +1177,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error on non-zero exit code", async () => { - const result = await h("pr.checks.rerun_all")( + const result = await h("pr.checks.rerun.all")( mockRunner(1, "", "rerun failed"), { owner: "o", name: "r", prNumber: 1, runId: 999 }, undefined, @@ -1324,9 +1187,9 @@ describe("pr domain handlers – additional coverage", () => { }) }) - describe("pr.review.request", () => { + describe("pr.reviews.request", () => { it("returns error for missing prNumber", async () => { - const result = await h("pr.review.request")( + const result = await h("pr.reviews.request")( mockRunner(0, ""), { owner: "o", name: "r", reviewers: ["alice"] }, undefined, @@ -1336,7 +1199,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error for empty reviewers", async () => { - const result = await h("pr.review.request")( + const result = await h("pr.reviews.request")( mockRunner(0, ""), { owner: "o", name: "r", prNumber: 1, reviewers: [] }, undefined, @@ -1346,7 +1209,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error when reviewers is not an array", async () => { - const result = await h("pr.review.request")( + const result = await h("pr.reviews.request")( mockRunner(0, ""), { owner: "o", name: "r", prNumber: 1, reviewers: "alice" }, undefined, @@ -1356,7 +1219,7 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error on non-zero exit code", async () => { - const result = await h("pr.review.request")( + const result = await h("pr.reviews.request")( mockRunner(1, "", "reviewer not found"), { owner: "o", name: "r", prNumber: 1, reviewers: ["alice"] }, undefined, @@ -1366,31 +1229,87 @@ describe("pr domain handlers – additional coverage", () => { }) }) - describe("pr.assignees.update", () => { + describe("pr.assignees.add", () => { + it("returns error for missing prNumber", async () => { + const result = await h("pr.assignees.add")( + mockRunner(0, ""), + { owner: "o", name: "r", assignees: ["alice"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("prNumber") + }) + + it("returns error for empty assignees", async () => { + const result = await h("pr.assignees.add")( + mockRunner(0, ""), + { owner: "o", name: "r", prNumber: 1, assignees: [] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("assignees") + }) + + it("returns error when assignees is not an array", async () => { + const result = await h("pr.assignees.add")( + mockRunner(0, ""), + { owner: "o", name: "r", prNumber: 1, assignees: "alice" }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("assignees") + }) + + it("returns error on non-zero exit code", async () => { + const result = await h("pr.assignees.add")( + mockRunner(1, "", "assignee error"), + { owner: "o", name: "r", prNumber: 1, assignees: ["alice"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.code).toBeDefined() + }) + + it("returns error when runner throws", async () => { + const runner = { + run: vi.fn().mockRejectedValue(new Error("timeout")), + } as unknown as CliCommandRunner + + const result = await h("pr.assignees.add")( + runner, + { owner: "o", name: "r", prNumber: 1, assignees: ["alice"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("timeout") + }) + }) + + describe("pr.assignees.remove", () => { it("returns error for missing prNumber", async () => { - const result = await h("pr.assignees.update")( + const result = await h("pr.assignees.remove")( mockRunner(0, ""), - { owner: "o", name: "r", add: ["alice"] }, + { owner: "o", name: "r", assignees: ["alice"] }, undefined, ) expect(result.ok).toBe(false) expect(result.error?.message).toContain("prNumber") }) - it("returns error for empty add and remove", async () => { - const result = await h("pr.assignees.update")( + it("returns error for empty assignees", async () => { + const result = await h("pr.assignees.remove")( mockRunner(0, ""), - { owner: "o", name: "r", prNumber: 1, add: [], remove: [] }, + { owner: "o", name: "r", prNumber: 1, assignees: [] }, undefined, ) expect(result.ok).toBe(false) expect(result.error?.message).toContain("assignees") }) - it("returns error when add is not an array and remove is not an array", async () => { - const result = await h("pr.assignees.update")( + it("returns error when assignees is not an array", async () => { + const result = await h("pr.assignees.remove")( mockRunner(0, ""), - { owner: "o", name: "r", prNumber: 1, add: "alice", remove: "bob" }, + { owner: "o", name: "r", prNumber: 1, assignees: "alice" }, undefined, ) expect(result.ok).toBe(false) @@ -1398,14 +1317,28 @@ describe("pr domain handlers – additional coverage", () => { }) it("returns error on non-zero exit code", async () => { - const result = await h("pr.assignees.update")( + const result = await h("pr.assignees.remove")( mockRunner(1, "", "assignee error"), - { owner: "o", name: "r", prNumber: 1, add: ["alice"] }, + { owner: "o", name: "r", prNumber: 1, assignees: ["alice"] }, undefined, ) expect(result.ok).toBe(false) expect(result.error?.code).toBeDefined() }) + + it("returns error when runner throws", async () => { + const runner = { + run: vi.fn().mockRejectedValue(new Error("timeout")), + } as unknown as CliCommandRunner + + const result = await h("pr.assignees.remove")( + runner, + { owner: "o", name: "r", prNumber: 1, assignees: ["alice"] }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("timeout") + }) }) describe("pr.branch.update", () => { @@ -1492,50 +1425,6 @@ describe("pr domain handlers – additional coverage", () => { expect(result.error?.message).toContain("Failed to parse CLI JSON output") }) }) - - describe("check_run.annotations.list", () => { - it("returns error for missing owner", async () => { - const result = await h("check_run.annotations.list")( - mockRunner(0, "[]"), - { owner: "", name: "repo", checkRunId: 1 }, - undefined, - ) - expect(result.ok).toBe(false) - expect(result.error?.message).toContain("Missing owner/name") - }) - - it("returns error for missing checkRunId", async () => { - const result = await h("check_run.annotations.list")( - mockRunner(0, "[]"), - { owner: "owner", name: "repo" }, - undefined, - ) - expect(result.ok).toBe(false) - expect(result.error?.message).toContain("checkRunId") - }) - - it("returns error on malformed JSON", async () => { - const result = await h("check_run.annotations.list")( - mockRunner(0, "not-json"), - { owner: "owner", name: "repo", checkRunId: 1 }, - undefined, - ) - expect(result.ok).toBe(false) - expect(result.error?.message).toContain("Failed to parse CLI JSON output") - }) - - it("handles non-object annotation items gracefully", async () => { - const result = await h("check_run.annotations.list")( - mockRunner(0, JSON.stringify([null, "bad", 42])), - { owner: "owner", name: "repo", checkRunId: 1 }, - undefined, - ) - expect(result.ok).toBe(true) - const items = (result.data as { items: unknown[] }).items - expect(items).toHaveLength(3) - expect(items[0]).toMatchObject({ path: null, message: null }) - }) - }) }) describe("pr domain handlers – null owner/name ?? branch coverage", () => { @@ -1574,15 +1463,6 @@ describe("pr domain handlers – null owner/name ?? branch coverage", () => { expect(result.ok).toBe(false) }) - it("pr.checks.failed covers owner/name null branches", async () => { - const result = await h("pr.checks.failed")( - nr(), - { owner: null, name: null, prNumber: 1 }, - undefined, - ) - expect(result.ok).toBe(false) - }) - it("pr.merge.status covers owner/name null branches", async () => { const result = await h("pr.merge.status")( nr(), @@ -1592,8 +1472,8 @@ describe("pr domain handlers – null owner/name ?? branch coverage", () => { expect(result.ok).toBe(false) }) - it("pr.review.submit covers owner/name null branches", async () => { - const result = await h("pr.review.submit")( + it("pr.reviews.submit covers owner/name null branches", async () => { + const result = await h("pr.reviews.submit")( nr(), { owner: null, name: null, prNumber: 1, event: "APPROVE" }, undefined, @@ -1606,8 +1486,8 @@ describe("pr domain handlers – null owner/name ?? branch coverage", () => { expect(result.ok).toBe(false) }) - it("pr.checks.rerun_failed covers owner/name null branches", async () => { - const result = await h("pr.checks.rerun_failed")( + it("pr.checks.rerun.failed covers owner/name null branches", async () => { + const result = await h("pr.checks.rerun.failed")( nr(), { owner: null, name: null, prNumber: 1 }, undefined, @@ -1615,8 +1495,8 @@ describe("pr domain handlers – null owner/name ?? branch coverage", () => { expect(result.ok).toBe(false) }) - it("pr.checks.rerun_all covers owner/name null branches", async () => { - const result = await h("pr.checks.rerun_all")( + it("pr.checks.rerun.all covers owner/name null branches", async () => { + const result = await h("pr.checks.rerun.all")( nr(), { owner: null, name: null, prNumber: 1 }, undefined, @@ -1624,8 +1504,8 @@ describe("pr domain handlers – null owner/name ?? branch coverage", () => { expect(result.ok).toBe(false) }) - it("pr.review.request covers owner/name null branches", async () => { - const result = await h("pr.review.request")( + it("pr.reviews.request covers owner/name null branches", async () => { + const result = await h("pr.reviews.request")( nr(), { owner: null, name: null, prNumber: 1, reviewers: ["alice"] }, undefined, @@ -1633,10 +1513,19 @@ describe("pr domain handlers – null owner/name ?? branch coverage", () => { expect(result.ok).toBe(false) }) - it("pr.assignees.update covers owner/name null branches", async () => { - const result = await h("pr.assignees.update")( + it("pr.assignees.add covers owner/name null branches", async () => { + const result = await h("pr.assignees.add")( + nr(), + { owner: null, name: null, prNumber: 1, assignees: ["alice"] }, + undefined, + ) + expect(result.ok).toBe(false) + }) + + it("pr.assignees.remove covers owner/name null branches", async () => { + const result = await h("pr.assignees.remove")( nr(), - { owner: null, name: null, prNumber: 1, add: ["alice"], remove: [] }, + { owner: null, name: null, prNumber: 1, assignees: ["alice"] }, undefined, ) expect(result.ok).toBe(false) @@ -1668,13 +1557,4 @@ describe("pr domain handlers – null owner/name ?? branch coverage", () => { ) expect(result.ok).toBe(false) }) - - it("check_run.annotations.list covers owner/name null branches", async () => { - const result = await h("check_run.annotations.list")( - nr(), - { owner: null, name: null, checkRunId: 1 }, - undefined, - ) - expect(result.ok).toBe(false) - }) }) diff --git a/packages/core/test/unit/cli-domains-project-v2.test.ts b/packages/core/test/unit/cli-domains-project-v2.test.ts index 5265d854..666cc508 100644 --- a/packages/core/test/unit/cli-domains-project-v2.test.ts +++ b/packages/core/test/unit/cli-domains-project-v2.test.ts @@ -18,7 +18,7 @@ const h = (id: string): CliHandler => { } describe("project-v2 domain handlers", () => { - describe("project_v2.org.get", () => { + describe("project_v2.org.view", () => { it("returns success with normalized summary", async () => { const runner = mockRunner( 0, @@ -32,7 +32,7 @@ describe("project-v2 domain handlers", () => { }), ) - const result = await h("project_v2.org.get")( + const result = await h("project_v2.org.view")( runner, { org: "myorg", @@ -50,7 +50,7 @@ describe("project-v2 domain handlers", () => { closed: false, url: "https://github.com/orgs/myorg/projects/1", }) - expect(result.meta.capability_id).toBe("project_v2.org.get") + expect(result.meta.capability_id).toBe("project_v2.org.view") expect(result.meta.route_used).toBe("cli") }) @@ -69,7 +69,7 @@ describe("project-v2 domain handlers", () => { }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.org.get")( + await h("project_v2.org.view")( runner, { org: "testorg", @@ -88,7 +88,7 @@ describe("project-v2 domain handlers", () => { it("returns error on non-zero exit code", async () => { const runner = mockRunner(1, "", "permission denied") - const result = await h("project_v2.org.get")( + const result = await h("project_v2.org.view")( runner, { org: "myorg", @@ -99,13 +99,13 @@ describe("project-v2 domain handlers", () => { expect(result.ok).toBe(false) expect(result.error?.code).toBeDefined() - expect(result.meta.capability_id).toBe("project_v2.org.get") + expect(result.meta.capability_id).toBe("project_v2.org.view") }) it("throws error for missing org", async () => { const runner = mockRunner(0, "{}") - const result = await h("project_v2.org.get")( + const result = await h("project_v2.org.view")( runner, { org: "", @@ -121,7 +121,7 @@ describe("project-v2 domain handlers", () => { it("throws error for missing projectNumber", async () => { const runner = mockRunner(0, "{}") - const result = await h("project_v2.org.get")( + const result = await h("project_v2.org.view")( runner, { org: "myorg", @@ -135,7 +135,7 @@ describe("project-v2 domain handlers", () => { }) }) - describe("project_v2.user.get", () => { + describe("project_v2.user.view", () => { it("returns success with normalized summary", async () => { const runner = mockRunner( 0, @@ -149,7 +149,7 @@ describe("project-v2 domain handlers", () => { }), ) - const result = await h("project_v2.user.get")( + const result = await h("project_v2.user.view")( runner, { user: "myuser", @@ -164,7 +164,7 @@ describe("project-v2 domain handlers", () => { title: "My Project", public: false, }) - expect(result.meta.capability_id).toBe("project_v2.user.get") + expect(result.meta.capability_id).toBe("project_v2.user.view") }) it("verifies call includes correct args", async () => { @@ -182,7 +182,7 @@ describe("project-v2 domain handlers", () => { }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.user.get")( + await h("project_v2.user.view")( runner, { user: "testuser", @@ -209,7 +209,7 @@ describe("project-v2 domain handlers", () => { it("returns error on non-zero exit code", async () => { const runner = mockRunner(1, "", "not found") - const result = await h("project_v2.user.get")( + const result = await h("project_v2.user.view")( runner, { user: "nonexistent", @@ -408,7 +408,7 @@ describe("project-v2 domain handlers", () => { }) }) - describe("project_v2.item.add_issue", () => { + describe("project_v2.items.issue.add", () => { it("returns success with added item", async () => { const runner = mockRunner( 0, @@ -418,7 +418,7 @@ describe("project-v2 domain handlers", () => { }), ) - const result = await h("project_v2.item.add_issue")( + const result = await h("project_v2.items.issue.add")( runner, { projectNumber: 1, @@ -433,7 +433,7 @@ describe("project-v2 domain handlers", () => { itemId: "PVT_I_1", added: true, }) - expect(result.meta.capability_id).toBe("project_v2.item.add_issue") + expect(result.meta.capability_id).toBe("project_v2.items.issue.add") }) it("verifies call includes correct args", async () => { @@ -444,7 +444,7 @@ describe("project-v2 domain handlers", () => { }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.add_issue")( + await h("project_v2.items.issue.add")( runner, { projectNumber: 1, @@ -474,7 +474,7 @@ describe("project-v2 domain handlers", () => { it("returns error on non-zero exit code", async () => { const runner = mockRunner(1, "", "issue not found") - const result = await h("project_v2.item.add_issue")( + const result = await h("project_v2.items.issue.add")( runner, { projectNumber: 1, @@ -489,11 +489,11 @@ describe("project-v2 domain handlers", () => { }) }) - describe("project_v2.item.field.update", () => { + describe("project_v2.items.field.update", () => { it("returns success with valueText", async () => { const runner = mockRunner(0, JSON.stringify({})) - const result = await h("project_v2.item.field.update")( + const result = await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -509,7 +509,7 @@ describe("project-v2 domain handlers", () => { itemId: "PVT_I_1", updated: true, }) - expect(result.meta.capability_id).toBe("project_v2.item.field.update") + expect(result.meta.capability_id).toBe("project_v2.items.field.update") }) it("includes valueText in args", async () => { @@ -520,7 +520,7 @@ describe("project-v2 domain handlers", () => { }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.field.update")( + await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -552,7 +552,7 @@ describe("project-v2 domain handlers", () => { it("returns success with valueNumber", async () => { const runner = mockRunner(0, JSON.stringify({})) - const result = await h("project_v2.item.field.update")( + const result = await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -578,7 +578,7 @@ describe("project-v2 domain handlers", () => { }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.field.update")( + await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -610,7 +610,7 @@ describe("project-v2 domain handlers", () => { it("returns success with clear=true", async () => { const runner = mockRunner(0, JSON.stringify({})) - const result = await h("project_v2.item.field.update")( + const result = await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -636,7 +636,7 @@ describe("project-v2 domain handlers", () => { }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.field.update")( + await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -667,7 +667,7 @@ describe("project-v2 domain handlers", () => { it("returns error when no field value provided", async () => { const runner = mockRunner(0, JSON.stringify({})) - const result = await h("project_v2.item.field.update")( + const result = await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -684,7 +684,7 @@ describe("project-v2 domain handlers", () => { it("returns error on non-zero exit code", async () => { const runner = mockRunner(1, "", "failed to update field") - const result = await h("project_v2.item.field.update")( + const result = await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -707,7 +707,7 @@ describe("project-v2 domain handlers", () => { }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.field.update")( + await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -730,7 +730,7 @@ describe("project-v2 domain handlers", () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "{}", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.field.update")( + await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", itemId: "PVT_I_1", fieldId: "F_4", valueDate: "2024-03-01" }, undefined, @@ -747,7 +747,7 @@ describe("project-v2 domain handlers", () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "{}", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.field.update")( + await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -769,7 +769,7 @@ describe("project-v2 domain handlers", () => { const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "{}", stderr: "" }) const runner = { run: runSpy } as unknown as CliCommandRunner - await h("project_v2.item.field.update")( + await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", @@ -789,8 +789,8 @@ describe("project-v2 domain handlers", () => { }) describe("SyntaxError paths", () => { - it("project_v2.org.get returns error on malformed JSON", async () => { - const result = await h("project_v2.org.get")( + it("project_v2.org.view returns error on malformed JSON", async () => { + const result = await h("project_v2.org.view")( mockRunner(0, "not-json"), { org: "myorg", projectNumber: 1 }, undefined, @@ -799,8 +799,8 @@ describe("project-v2 domain handlers", () => { expect(result.error?.message).toContain("Failed to parse CLI JSON output") }) - it("project_v2.user.get returns error on malformed JSON", async () => { - const result = await h("project_v2.user.get")( + it("project_v2.user.view returns error on malformed JSON", async () => { + const result = await h("project_v2.user.view")( mockRunner(0, "not-json"), { user: "myuser", projectNumber: 1 }, undefined, @@ -829,8 +829,8 @@ describe("project-v2 domain handlers", () => { expect(result.error?.message).toContain("Failed to parse CLI JSON output") }) - it("project_v2.item.add_issue returns error on malformed JSON", async () => { - const result = await h("project_v2.item.add_issue")( + it("project_v2.items.issue.add returns error on malformed JSON", async () => { + const result = await h("project_v2.items.issue.add")( mockRunner(0, "not-json"), { owner: "myorg", projectNumber: 1, issueUrl: "https://github.com/myorg/repo/issues/1" }, undefined, @@ -841,8 +841,8 @@ describe("project-v2 domain handlers", () => { }) describe("missing params paths", () => { - it("project_v2.user.get returns error for missing projectNumber", async () => { - const result = await h("project_v2.user.get")( + it("project_v2.user.view returns error for missing projectNumber", async () => { + const result = await h("project_v2.user.view")( mockRunner(0, "{}"), { user: "myuser", projectNumber: 0 }, undefined, @@ -869,8 +869,8 @@ describe("project-v2 domain handlers", () => { expect(result.ok).toBe(false) }) - it("project_v2.item.add_issue returns error for missing issueUrl", async () => { - const result = await h("project_v2.item.add_issue")( + it("project_v2.items.issue.add returns error for missing issueUrl", async () => { + const result = await h("project_v2.items.issue.add")( mockRunner(0, "{}"), { owner: "myorg", projectNumber: 1, issueUrl: "" }, undefined, @@ -878,8 +878,8 @@ describe("project-v2 domain handlers", () => { expect(result.ok).toBe(false) }) - it("project_v2.item.field.update returns error for missing projectId", async () => { - const result = await h("project_v2.item.field.update")( + it("project_v2.items.field.update returns error for missing projectId", async () => { + const result = await h("project_v2.items.field.update")( mockRunner(0, "{}"), { projectId: "", itemId: "PVT_I_1", fieldId: "F_1", valueText: "x" }, undefined, @@ -887,8 +887,8 @@ describe("project-v2 domain handlers", () => { expect(result.ok).toBe(false) }) - it("project_v2.user.get returns error for missing user", async () => { - const result = await h("project_v2.user.get")( + it("project_v2.user.view returns error for missing user", async () => { + const result = await h("project_v2.user.view")( mockRunner(0, "{}"), { user: "", projectNumber: 1 }, undefined, @@ -897,12 +897,12 @@ describe("project-v2 domain handlers", () => { expect(result.error?.message).toContain("user") }) - it("project_v2.item.field.update returns error when runner throws", async () => { + it("project_v2.items.field.update returns error when runner throws", async () => { const runner = { run: vi.fn().mockRejectedValue(new Error("runner failure")), } as unknown as import("@core/core/execution/adapters/cli-adapter.js").CliCommandRunner - const result = await h("project_v2.item.field.update")( + const result = await h("project_v2.items.field.update")( runner, { projectId: "PVT_1", itemId: "PVT_I_1", fieldId: "F_1", valueText: "x" }, undefined, @@ -934,5 +934,101 @@ describe("project-v2 domain handlers", () => { expect(items).toHaveLength(2) expect(items[0]).toMatchObject({ id: null, contentType: null }) }) + + describe("project_v2.items.issue.remove", () => { + it("returns success with itemId and removed: true", async () => { + const result = await h("project_v2.items.issue.remove")( + mockRunner(0, ""), + { owner: "myorg", projectNumber: 123, itemId: "PVTI_abc123" }, + undefined, + ) + expect(result.ok).toBe(true) + expect(result.data).toMatchObject({ itemId: "PVTI_abc123", removed: true }) + expect(result.meta.capability_id).toBe("project_v2.items.issue.remove") + expect(result.meta.route_used).toBe("cli") + }) + + it("verifies correct gh args are used", async () => { + const runSpy = vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) + const runner = { + run: runSpy, + } as unknown as import("@core/core/execution/adapters/cli-adapter.js").CliCommandRunner + + await h("project_v2.items.issue.remove")( + runner, + { owner: "myorg", projectNumber: 123, itemId: "PVTI_abc123" }, + undefined, + ) + + expect(runSpy).toHaveBeenCalledWith( + "gh", + expect.arrayContaining([ + "project", + "item-delete", + "123", + "--owner", + "myorg", + "--id", + "PVTI_abc123", + ]), + expect.any(Number), + ) + }) + + it("returns error for missing owner", async () => { + const result = await h("project_v2.items.issue.remove")( + mockRunner(0, ""), + { owner: "", projectNumber: 123, itemId: "PVTI_abc123" }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("owner") + }) + + it("returns error for missing projectNumber", async () => { + const result = await h("project_v2.items.issue.remove")( + mockRunner(0, ""), + { owner: "myorg", projectNumber: 0, itemId: "PVTI_abc123" }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("projectNumber") + }) + + it("returns error for missing itemId", async () => { + const result = await h("project_v2.items.issue.remove")( + mockRunner(0, ""), + { owner: "myorg", projectNumber: 123, itemId: "" }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("itemId") + }) + + it("returns error on non-zero exit code", async () => { + const result = await h("project_v2.items.issue.remove")( + mockRunner(1, "", "item not found"), + { owner: "myorg", projectNumber: 123, itemId: "PVTI_abc123" }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.code).toBeDefined() + expect(result.meta.capability_id).toBe("project_v2.items.issue.remove") + }) + + it("returns error when runner throws", async () => { + const runner = { + run: vi.fn().mockRejectedValue(new Error("runner failure")), + } as unknown as import("@core/core/execution/adapters/cli-adapter.js").CliCommandRunner + + const result = await h("project_v2.items.issue.remove")( + runner, + { owner: "myorg", projectNumber: 123, itemId: "PVTI_abc123" }, + undefined, + ) + expect(result.ok).toBe(false) + expect(result.error?.message).toContain("runner failure") + }) + }) }) }) diff --git a/packages/core/test/unit/cli-domains-release.test.ts b/packages/core/test/unit/cli-domains-release.test.ts index d2efded1..9e1a2b05 100644 --- a/packages/core/test/unit/cli-domains-release.test.ts +++ b/packages/core/test/unit/cli-domains-release.test.ts @@ -128,7 +128,7 @@ describe("release domain handlers", () => { isPrerelease: false, url: "https://github.com/owner/repo/releases/tag/v1.0.0", }) - expect(result.meta.capability_id).toBe("release.get") + expect(result.meta.capability_id).toBe("release.view") }) it("returns error on non-zero exit code", async () => { @@ -180,7 +180,7 @@ describe("release domain handlers", () => { tagName: "v2.0.0", isDraft: true, }) - expect(result.meta.capability_id).toBe("release.create_draft") + expect(result.meta.capability_id).toBe("release.create") }) it("returns error on non-zero exit code", async () => { @@ -305,7 +305,7 @@ describe("release domain handlers", () => { isDraft: false, wasDraft: true, }) - expect(result.meta.capability_id).toBe("release.publish_draft") + expect(result.meta.capability_id).toBe("release.publish") }) it("returns error when first read call fails", async () => { @@ -319,7 +319,7 @@ describe("release domain handlers", () => { expect(result.ok).toBe(false) expect(result.error?.code).toBeDefined() - expect(result.meta.capability_id).toBe("release.publish_draft") + expect(result.meta.capability_id).toBe("release.publish") }) it("returns Validation error when release is not a draft", async () => { @@ -394,10 +394,10 @@ describe("release domain handlers", () => { describe("handlers export", () => { it("exports all release handlers", () => { expect(handlers["release.list"]).toBe(handleReleaseList) - expect(handlers["release.get"]).toBe(handleReleaseGet) - expect(handlers["release.create_draft"]).toBe(handleReleaseCreateDraft) + expect(handlers["release.view"]).toBe(handleReleaseGet) + expect(handlers["release.create"]).toBe(handleReleaseCreateDraft) expect(handlers["release.update"]).toBe(handleReleaseUpdate) - expect(handlers["release.publish_draft"]).toBe(handleReleasePublishDraft) + expect(handlers["release.publish"]).toBe(handleReleasePublishDraft) }) }) }) @@ -424,7 +424,7 @@ describe("release domain handlers – additional coverage", () => { expect(result.error?.message).toContain("owner/name") }) - it("release.create_draft returns error for missing owner", async () => { + it("release.create returns error for missing owner", async () => { const result = await handleReleaseCreateDraft( mockRunner(0, "{}"), { owner: "", name: "repo", tagName: "v1.0.0" }, @@ -444,7 +444,7 @@ describe("release domain handlers – additional coverage", () => { expect(result.error?.message).toContain("owner/name") }) - it("release.publish_draft returns error for missing owner", async () => { + it("release.publish returns error for missing owner", async () => { const result = await handleReleasePublishDraft( mockRunner(0, "{}"), { owner: "", name: "repo", releaseId: 1 }, @@ -476,7 +476,7 @@ describe("release domain handlers – additional coverage", () => { expect(result.error?.message).toContain("Failed to parse CLI JSON output") }) - it("release.create_draft returns error on malformed JSON", async () => { + it("release.create returns error on malformed JSON", async () => { const result = await handleReleaseCreateDraft( mockRunner(0, "not-json"), { owner: "acme", name: "repo", tagName: "v1.0.0" }, @@ -496,7 +496,7 @@ describe("release domain handlers – additional coverage", () => { expect(result.error?.message).toContain("Failed to parse CLI JSON output") }) - it("release.publish_draft returns error on malformed JSON in read step", async () => { + it("release.publish returns error on malformed JSON in read step", async () => { const result = await handleReleasePublishDraft( mockRunner(0, "not-json"), { owner: "acme", name: "repo", releaseId: 1 }, @@ -526,7 +526,7 @@ describe("release domain handlers – additional coverage", () => { expect(result.ok).toBe(false) }) - it("release.publish_draft returns error for missing releaseId", async () => { + it("release.publish returns error for missing releaseId", async () => { const result = await handleReleasePublishDraft( mockRunner(0, "{}"), { owner: "acme", name: "repo", releaseId: 0 }, @@ -636,7 +636,7 @@ describe("release domain handlers – additional coverage", () => { }) }) - describe("release.publish_draft – optional params and non-draft guard", () => { + describe("release.publish – optional params and non-draft guard", () => { it("includes notes, prerelease in publish call when provided", async () => { const runSpy = vi .fn() diff --git a/packages/core/test/unit/cli-domains-workflow.test.ts b/packages/core/test/unit/cli-domains-workflow.test.ts index 857b7615..959e1190 100644 --- a/packages/core/test/unit/cli-domains-workflow.test.ts +++ b/packages/core/test/unit/cli-domains-workflow.test.ts @@ -332,7 +332,7 @@ Final line` state: "active", url: "https://github.com/owner/repo/blob/main/.github/workflows/ci.yml", }) - expect(result.meta.capability_id).toBe("workflow.get") + expect(result.meta.capability_id).toBe("workflow.view") }) it("accepts workflowId as number", async () => { @@ -488,9 +488,9 @@ Final line` expect(result.ok).toBe(true) expect(result.data).toMatchObject({ runId: 123456, - status: "requested", + queued: true, }) - expect(result.meta.capability_id).toBe("workflow.run.rerun_all") + expect(result.meta.capability_id).toBe("workflow.run.rerun.all") }) it("returns error on non-zero exit code", async () => { @@ -625,7 +625,7 @@ Final line` ref: "main", dispatched: true, }) - expect(result.meta.capability_id).toBe("workflow.dispatch.run") + expect(result.meta.capability_id).toBe("workflow.dispatch") }) it("includes inputs in api call", async () => { @@ -719,9 +719,9 @@ Final line` expect(result.ok).toBe(true) expect(result.data).toMatchObject({ runId: 123456, - rerunFailed: true, + queued: true, }) - expect(result.meta.capability_id).toBe("workflow.run.rerun_failed") + expect(result.meta.capability_id).toBe("workflow.run.rerun.failed") }) it("uses api call with repos path", async () => { diff --git a/packages/core/test/unit/composite-engine.test.ts b/packages/core/test/unit/composite-engine.test.ts deleted file mode 100644 index 27a97988..00000000 --- a/packages/core/test/unit/composite-engine.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { TaskRequest } from "@core/core/contracts/task.js" -import { executeTask } from "@core/core/routing/engine.js" -import type { GithubClient } from "@core/gql/github-client.js" -import { beforeEach, describe, expect, it, vi } from "vitest" - -describe("composite execution in engine", () => { - let mockGithubClient: Partial - - beforeEach(() => { - mockGithubClient = { - query: vi.fn(), - fetchRepoView: vi.fn(async () => ({ - id: "repo-1", - name: "test", - nameWithOwner: "test/test", - isPrivate: false, - stargazerCount: 0, - forkCount: 0, - url: "https://example.com/test/test", - defaultBranch: "main", - })), - } - }) - - it("routes composite cards to composite execution path", async () => { - const queryMock = vi.fn().mockResolvedValue({ - pr_thread_reply_0: { comment: { id: "c1" } }, - pr_thread_resolve_1: { thread: { id: "t1", isResolved: true } }, - }) - mockGithubClient.query = queryMock - - const request: TaskRequest = { - task: "pr.threads.composite", - input: { - threads: [ - { threadId: "t1", action: "reply", body: "Fixed" }, - { threadId: "t1", action: "resolve" }, - ], - }, - } - - const result = await executeTask(request, { - githubClient: mockGithubClient as GithubClient, - githubToken: "token", - skipGhPreflight: true, - }) - - expect(result.ok).toBe(true) - expect(result.data).toEqual({ - results: [{ id: "c1" }, { id: "t1", isResolved: true }], - }) - expect(result).toHaveProperty("meta") - expect(result.meta.capability_id).toBe("pr.threads.composite") - }) - - it("returns validation error envelope for unknown action", async () => { - const request: TaskRequest = { - task: "pr.threads.composite", - input: { - threads: [{ threadId: "t1", action: "invalid_action", body: "Test" }], - }, - } - - const result = await executeTask(request, { - githubClient: mockGithubClient as GithubClient, - githubToken: "token", - skipGhPreflight: true, - }) - - expect(result.ok).toBe(false) - expect(result.error).toBeDefined() - expect(result.error?.code).toBe("VALIDATION") - expect(result.error?.message).toContain("Input schema validation failed") - }) - - it("falls through to normal execute for non-composite cards", async () => { - const request: TaskRequest = { - task: "repo.view", - input: { - owner: "test", - name: "test", - }, - } - - const result = await executeTask(request, { - githubClient: mockGithubClient as GithubClient, - githubToken: "token", - skipGhPreflight: true, - }) - - expect(result.ok).toBe(true) - expect(result.meta.capability_id).toBe("repo.view") - }) -}) diff --git a/packages/core/test/unit/composite-expand.test.ts b/packages/core/test/unit/composite-expand.test.ts deleted file mode 100644 index 137f463b..00000000 --- a/packages/core/test/unit/composite-expand.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { expandCompositeSteps } from "@core/core/execute/composite.js" -import type { CompositeConfig } from "@core/core/registry/types.js" -import { describe, expect, it } from "vitest" - -describe("expandCompositeSteps", () => { - it("throws when foreach key is not an array", () => { - const composite: CompositeConfig = { - steps: [ - { - capability_id: "pr.thread.reply", - foreach: "threads", - params_map: { threadId: "threadId", body: "body" }, - }, - ], - output_strategy: "array", - } - - expect(() => expandCompositeSteps(composite, { threads: "bad" as unknown as [] })).toThrow( - 'Composite foreach key "threads" must be an array, got string', - ) - }) - - it("uses all step capability ids when action is absent", () => { - const composite: CompositeConfig = { - steps: [ - { - capability_id: "pr.thread.reply", - params_map: { threadId: "threadId", body: "body" }, - }, - { - capability_id: "pr.thread.resolve", - params_map: { threadId: "threadId" }, - }, - ], - output_strategy: "array", - } - - const operations = expandCompositeSteps(composite, { - threadId: "T_1", - body: "reply body", - }) - - expect(operations).toHaveLength(2) - expect(operations.map((op) => op.alias)).toEqual(["pr_thread_reply_0", "pr_thread_resolve_1"]) - }) - - it("throws when a step has no registered operation builder", () => { - const composite: CompositeConfig = { - steps: [ - { - capability_id: "pr.thread.reply", - params_map: { threadId: "threadId", body: "body" }, - }, - { - capability_id: "pr.thread.missing", - params_map: { threadId: "threadId" }, - }, - ], - output_strategy: "array", - } - - expect(() => - expandCompositeSteps(composite, { - threadId: "T_1", - body: "reply body", - }), - ).toThrow("No builder registered for capability: pr.thread.missing") - }) - - it("throws when action is not mapped by composite steps", () => { - const composite: CompositeConfig = { - steps: [ - { - capability_id: "pr.thread.reply", - foreach: "threads", - actions: ["reply", "reply_and_resolve"], - params_map: { threadId: "threadId", body: "body" }, - }, - { - capability_id: "pr.thread.resolve", - foreach: "threads", - actions: ["resolve", "reply_and_resolve"], - params_map: { threadId: "threadId" }, - }, - ], - output_strategy: "array", - } - - expect(() => - expandCompositeSteps(composite, { - threads: [{ threadId: "T_1", action: "unknown_action", body: "reply body" }], - }), - ).toThrow('Invalid action "unknown_action" for composite item at index 0') - }) - - it("throws when foreach contains non-object items", () => { - const composite: CompositeConfig = { - steps: [ - { - capability_id: "pr.thread.resolve", - foreach: "threads", - params_map: { threadId: "threadId" }, - }, - ], - output_strategy: "array", - } - - expect(() => - expandCompositeSteps(composite, { - threads: ["bad-item"], - }), - ).toThrow("Composite foreach item at index 0 must be an object") - }) - - it("skips step when requires_any_of fields are all missing", () => { - const composite: CompositeConfig = { - steps: [ - { - capability_id: "pr.thread.reply", - requires_any_of: ["body"], - params_map: { threadId: "threadId", body: "body" }, - }, - { - capability_id: "pr.thread.resolve", - params_map: { threadId: "threadId" }, - }, - ], - output_strategy: "array", - } - - const operations = expandCompositeSteps(composite, { threadId: "T_1" }) - expect(operations).toHaveLength(1) - expect(operations[0]?.alias).toBe("pr_thread_resolve_0") - }) -}) diff --git a/packages/core/test/unit/composite-types.test.ts b/packages/core/test/unit/composite-types.test.ts deleted file mode 100644 index 6815ba71..00000000 --- a/packages/core/test/unit/composite-types.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { OperationCard } from "@core/core/registry/types.js" -import { describe, expect, it } from "vitest" - -describe("CompositeConfig types", () => { - it("allows OperationCard with composite field", () => { - const card: OperationCard = { - capability_id: "pr.threads.composite", - version: "1.0.0", - description: "Batch thread operations", - input_schema: { type: "object" }, - output_schema: { type: "object" }, - routing: { preferred: "graphql", fallbacks: [] }, - composite: { - steps: [ - { - capability_id: "pr.thread.reply", - foreach: "threads", - params_map: { threadId: "threadId", body: "body" }, - }, - ], - output_strategy: "array", - }, - } - expect(card.composite).toBeDefined() - if (!card.composite) return - expect(card.composite.steps).toHaveLength(1) - expect(card.composite.output_strategy).toBe("array") - }) - - it("allows OperationCard without composite field", () => { - const card: OperationCard = { - capability_id: "repo.view", - version: "1.0.0", - description: "View repo", - input_schema: { type: "object" }, - output_schema: { type: "object" }, - routing: { preferred: "graphql", fallbacks: [] }, - } - expect(card.composite).toBeUndefined() - }) -}) diff --git a/packages/core/test/unit/engine.test.ts b/packages/core/test/unit/engine.test.ts index b21d435b..d96227b3 100644 --- a/packages/core/test/unit/engine.test.ts +++ b/packages/core/test/unit/engine.test.ts @@ -25,29 +25,6 @@ const baseCard: OperationCard = { }, } -const compositeCard: OperationCard = { - capability_id: "pr.threads.composite", - version: "1.0.0", - description: "Composite review thread operations", - input_schema: { type: "object" }, - output_schema: { type: "object" }, - routing: { - preferred: "graphql", - fallbacks: [], - }, - composite: { - steps: [ - { - capability_id: "pr.thread.reply", - foreach: "threads", - actions: ["reply"], - params_map: { threadId: "threadId", body: "body" }, - }, - ], - output_strategy: "array", - }, -} - function createGithubClient(overrides?: Partial): GithubClient { return { fetchRepoView: vi.fn(), @@ -150,27 +127,6 @@ describe("executeTask engine wiring", () => { expect(result).toEqual({ ok: true }) }) - it("uses execute() pipeline for composite cards", async () => { - getOperationCardMock.mockReturnValue(compositeCard) - executeMock.mockResolvedValue({ ok: true }) - - const { executeTask } = await import("@core/core/routing/engine.js") - - const result = await executeTask( - { - task: "pr.threads.composite", - input: { threads: [{ threadId: "T", action: "reply", body: "x" }] }, - }, - { - githubClient: createGithubClient(), - skipGhPreflight: true, - }, - ) - - expect(executeMock).toHaveBeenCalledTimes(1) - expect(result).toEqual({ ok: true }) - }) - it("detects missing CLI and returns cli preflight failure", async () => { const cliRunner = { run: vi.fn(async () => ({ @@ -238,154 +194,6 @@ describe("executeTask engine wiring", () => { ) }) - it("aggregates composite graphql result using merge strategy", async () => { - getOperationCardMock.mockReturnValue({ - ...compositeCard, - composite: { - ...compositeCard.composite, - output_strategy: "merge", - }, - }) - - executeMock.mockImplementation( - async (options: { routes: { graphql: () => Promise } }) => options.routes.graphql(), - ) - - const { executeTask } = await import("@core/core/routing/engine.js") - const result = await executeTask( - { - task: "pr.threads.composite", - input: { threads: [{ threadId: "T", action: "reply", body: "x" }] }, - }, - { - githubClient: createGithubClient({ - query: vi.fn().mockResolvedValue({ - pr_thread_reply_0: { comment: { id: "c1" } }, - }), - }), - githubToken: "token", - skipGhPreflight: true, - }, - ) - - expect(result.ok).toBe(true) - expect(result.data).toEqual({ id: "c1" }) - }) - - it("aggregates composite graphql result using last strategy", async () => { - getOperationCardMock.mockReturnValue({ - ...compositeCard, - composite: { - ...compositeCard.composite, - output_strategy: "last", - }, - }) - - executeMock.mockImplementation( - async (options: { routes: { graphql: () => Promise } }) => options.routes.graphql(), - ) - - const { executeTask } = await import("@core/core/routing/engine.js") - const result = await executeTask( - { - task: "pr.threads.composite", - input: { threads: [{ threadId: "T", action: "reply", body: "x" }] }, - }, - { - githubClient: createGithubClient({ - query: vi.fn().mockResolvedValue({ - pr_thread_reply_0: { comment: { id: "c1" } }, - }), - }), - githubToken: "token", - skipGhPreflight: true, - }, - ) - - expect(result.ok).toBe(true) - expect(result.data).toEqual({ id: "c1" }) - }) - - it("normalizes composite graphql mapping failures", async () => { - getOperationCardMock.mockReturnValue(compositeCard) - - executeMock.mockImplementation( - async (options: { routes: { graphql: () => Promise } }) => options.routes.graphql(), - ) - - const { executeTask } = await import("@core/core/routing/engine.js") - const result = await executeTask( - { - task: "pr.threads.composite", - input: { threads: [{ threadId: "T", action: "reply", body: "x" }] }, - }, - { - githubClient: createGithubClient({ - query: vi.fn().mockResolvedValue({ - pr_thread_reply_0: {}, - }), - }), - githubToken: "token", - skipGhPreflight: true, - }, - ) - - expect(result.ok).toBe(false) - expect(result.error?.message).toContain("Review thread mutation failed") - expect(result.meta.route_used).toBe("graphql") - }) - - it("returns explicit validation error when an operation alias is missing from batch response", async () => { - getOperationCardMock.mockReturnValue(compositeCard) - - executeMock.mockImplementation( - async (options: { routes: { graphql: () => Promise } }) => options.routes.graphql(), - ) - - const { executeTask } = await import("@core/core/routing/engine.js") - const result = await executeTask( - { - task: "pr.threads.composite", - input: { threads: [{ threadId: "T", action: "reply", body: "x" }] }, - }, - { - githubClient: createGithubClient({ - query: vi.fn().mockResolvedValue({}), - }), - githubToken: "token", - skipGhPreflight: true, - }, - ) - - expect(result.ok).toBe(false) - expect(result.error?.message).toContain('Missing result for alias "pr_thread_reply_0"') - }) - - it("returns validation error when composite receives unknown action", async () => { - getOperationCardMock.mockReturnValue(compositeCard) - executeMock.mockImplementation( - async (options: { routes: { graphql: () => Promise } }) => options.routes.graphql(), - ) - - const { executeTask } = await import("@core/core/routing/engine.js") - const result = await executeTask( - { - task: "pr.threads.composite", - input: { threads: [{ threadId: "T", action: "invalid_action", body: "x" }] }, - }, - { - githubClient: createGithubClient({ - query: vi.fn(), - }), - githubToken: "token", - skipGhPreflight: true, - }, - ) - - expect(result.ok).toBe(false) - expect(result.error?.message).toContain('Invalid action "invalid_action"') - }) - it("handles cached CLI probe post-processing errors by clearing in-flight entry", async () => { const cliRunner = { run: vi @@ -435,36 +243,4 @@ describe("executeTask engine wiring", () => { nowSpy.mockRestore() }) - - it("defensively handles cards that lose composite config after routing check", async () => { - const proxyCard = { - ...compositeCard, - get composite() { - this.__reads = (this.__reads ?? 0) + 1 - return this.__reads === 1 ? compositeCard.composite : undefined - }, - __reads: 0, - } as OperationCard & { __reads: number } - getOperationCardMock.mockReturnValue(proxyCard) - - executeMock.mockImplementation( - async (options: { routes: { graphql: () => Promise } }) => options.routes.graphql(), - ) - - const { executeTask } = await import("@core/core/routing/engine.js") - const result = await executeTask( - { - task: "pr.threads.composite", - input: { threads: [{ threadId: "T", action: "reply", body: "x" }] }, - }, - { - githubClient: createGithubClient(), - githubToken: "token", - skipGhPreflight: true, - }, - ) - - expect(result.ok).toBe(false) - expect(result.error?.message).toContain("Card does not have composite config") - }) }) diff --git a/packages/core/test/unit/github-client.test.ts b/packages/core/test/unit/github-client.test.ts index 7aa4b063..1985b616 100644 --- a/packages/core/test/unit/github-client.test.ts +++ b/packages/core/test/unit/github-client.test.ts @@ -229,7 +229,7 @@ describe("createGithubClient", () => { expect(list.pageInfo.hasNextPage).toBe(true) }) - it("exposes typed pr.thread.list helper with unresolved filtering", async () => { + it("exposes typed pr.threads.list helper with unresolved filtering", async () => { const execute = async ( _query: string, variables?: Record, @@ -500,7 +500,7 @@ describe("createGithubClient", () => { expect(list.pageInfo.endCursor).toBe("cursor-2") }) - it("throws when pr.thread.list payload is missing threads", async () => { + it("throws when pr.threads.list payload is missing threads", async () => { const client = createGithubClient({ async execute(): Promise { return { @@ -521,7 +521,7 @@ describe("createGithubClient", () => { ).rejects.toThrow("Pull request review threads not found") }) - it("throws when pr.thread.list unresolvedOnly/includeOutdated has invalid type", async () => { + it("throws when pr.threads.list unresolvedOnly/includeOutdated has invalid type", async () => { const client = createGithubClient({ async execute(): Promise { return { @@ -561,7 +561,7 @@ describe("createGithubClient", () => { ).rejects.toThrow("includeOutdated must be a boolean") }) - it("exposes typed pr.review.list helper", async () => { + it("exposes typed pr.reviews.list helper", async () => { const client = createGithubClient({ async execute(): Promise { return { @@ -637,7 +637,7 @@ describe("createGithubClient", () => { expect(files.items[0]?.additions).toBe(5) }) - it("throws when pr.review.list payload is missing reviews", async () => { + it("throws when pr.reviews.list payload is missing reviews", async () => { const client = createGithubClient({ async execute(): Promise { return { @@ -1120,6 +1120,8 @@ describe("createGithubClient", () => { ).resolves.toEqual({ id: "thread-1", isResolved: true, + commentId: "comment-1", + commentUrl: "", }) await expect(client.resolveReviewThread({ threadId: "thread-1" })).resolves.toEqual({ id: "thread-1", @@ -1131,7 +1133,7 @@ describe("createGithubClient", () => { }) }) - it("throws when pr.thread.reply mutation payload is missing comment", async () => { + it("throws when pr.threads.reply mutation payload is missing comment", async () => { const client = createGithubClient({ async execute(query: string): Promise { if (query.includes("mutation PrCommentReply")) { @@ -1156,7 +1158,7 @@ describe("createGithubClient", () => { ).rejects.toThrow("Review thread mutation failed") }) - it("throws when pr.thread.reply thread-state lookup returns no node", async () => { + it("throws when pr.threads.reply thread-state lookup returns no node", async () => { const client = createGithubClient({ async execute(query: string): Promise { if (query.includes("mutation PrCommentReply")) { @@ -1178,7 +1180,7 @@ describe("createGithubClient", () => { ).rejects.toThrow("Review thread state lookup failed") }) - it("throws when pr.thread.resolve payload has no thread", async () => { + it("throws when pr.threads.resolve payload has no thread", async () => { const client = createGithubClient({ async execute(): Promise { return { @@ -1213,7 +1215,7 @@ describe("createGithubClient", () => { ) }) - it("validates non-empty body for pr.thread.reply", async () => { + it("validates non-empty body for pr.threads.reply", async () => { const client = createGithubClient({ async execute(): Promise { return { diff --git a/packages/core/test/unit/gql-builders.test.ts b/packages/core/test/unit/gql-builders.test.ts index e1c40a5a..c72e62e0 100644 --- a/packages/core/test/unit/gql-builders.test.ts +++ b/packages/core/test/unit/gql-builders.test.ts @@ -36,7 +36,7 @@ describe("OperationBuilder exports", () => { }) }) -describe("pr.thread.reply builder", () => { +describe("pr.threads.reply builder", () => { it("build() returns mutation string and variables", () => { const result = replyBuilder.build({ threadId: "t1", body: "Fixed" }) expect(result.mutation).toContain("addPullRequestReviewThreadReply") @@ -50,11 +50,22 @@ describe("pr.thread.reply builder", () => { it("mapResponse() extracts comment id", () => { const raw = { comment: { id: "c1" } } const result = replyBuilder.mapResponse(raw) - expect(result).toEqual({ id: "c1" }) + expect(result).toEqual({ id: "c1", isResolved: false, commentId: "c1", commentUrl: "" }) + }) + + it("mapResponse() includes commentUrl when comment.url is a string", () => { + const raw = { comment: { id: "c2", url: "https://github.com/owner/repo/pull/1#comment-42" } } + const result = replyBuilder.mapResponse(raw) + expect(result).toEqual({ + id: "c2", + isResolved: false, + commentId: "c2", + commentUrl: "https://github.com/owner/repo/pull/1#comment-42", + }) }) }) -describe("pr.thread.resolve builder", () => { +describe("pr.threads.resolve builder", () => { it("build() returns mutation string and variables", () => { const result = resolveBuilder.build({ threadId: "t1" }) expect(result.mutation).toContain("resolveReviewThread") @@ -68,7 +79,7 @@ describe("pr.thread.resolve builder", () => { }) }) -describe("pr.thread.unresolve builder", () => { +describe("pr.threads.unresolve builder", () => { it("build() returns mutation string and variables", () => { const result = unresolveBuilder.build({ threadId: "t1" }) expect(result.mutation).toContain("unresolveReviewThread") @@ -121,8 +132,8 @@ describe("issue builders", () => { }) }) - it("issue.labels.update validates label ids and maps names", () => { - const builder = expectBuilder("issue.labels.update") + it("issue.labels.set validates label ids and maps names", () => { + const builder = expectBuilder("issue.labels.set") expect(() => builder.build({ issueId: "i1", labelIds: [1] })).toThrow( "labelIds (or labels) must be an array of strings", ) @@ -138,8 +149,8 @@ describe("issue builders", () => { expect(mapped).toEqual({ issueId: "i1", labels: ["bug"] }) }) - it("issue.assignees.update validates ids and maps logins", () => { - const builder = expectBuilder("issue.assignees.update") + it("issue.assignees.set validates ids and maps logins", () => { + const builder = expectBuilder("issue.assignees.set") expect(() => builder.build({ issueId: "i1", assigneeIds: [true] })).toThrow( "assigneeIds (or assignees) must be an array of strings", ) diff --git a/packages/core/test/unit/gql-capability-registry.test.ts b/packages/core/test/unit/gql-capability-registry.test.ts index 49a73a46..86fffcc3 100644 --- a/packages/core/test/unit/gql-capability-registry.test.ts +++ b/packages/core/test/unit/gql-capability-registry.test.ts @@ -4,10 +4,10 @@ import { describe, expect, it, vi } from "vitest" describe("gql capability registry", () => { it("lists capabilities and validates submit handler availability", async () => { const capabilities = listGraphqlCapabilities() - expect(capabilities).toContain("pr.review.submit") + expect(capabilities).toContain("pr.reviews.submit") expect(capabilities).toContain("issue.labels.add") - const submitHandler = getGraphqlHandler("pr.review.submit") + const submitHandler = getGraphqlHandler("pr.reviews.submit") expect(submitHandler).toBeDefined() if (!submitHandler) { throw new Error("missing submit handler") diff --git a/packages/core/test/unit/graphql-capability-adapter.test.ts b/packages/core/test/unit/graphql-capability-adapter.test.ts index f53ff28b..22bbf0f6 100644 --- a/packages/core/test/unit/graphql-capability-adapter.test.ts +++ b/packages/core/test/unit/graphql-capability-adapter.test.ts @@ -180,7 +180,7 @@ describe("runGraphqlCapability", () => { ) }) - it("routes pr.thread.list through the GraphQL client", async () => { + it("routes pr.threads.list through the GraphQL client", async () => { const client = { fetchRepoView: vi.fn(), fetchIssueView: vi.fn(), @@ -229,13 +229,17 @@ describe("runGraphqlCapability", () => { submitPrReview: vi.fn(), } - const result = await runGraphqlCapability(client as unknown as GithubClient, "pr.thread.list", { - owner: "acme", - name: "modkit", - prNumber: 1, - unresolvedOnly: true, - includeOutdated: false, - }) + const result = await runGraphqlCapability( + client as unknown as GithubClient, + "pr.threads.list", + { + owner: "acme", + name: "modkit", + prNumber: 1, + unresolvedOnly: true, + includeOutdated: false, + }, + ) expect(result.ok).toBe(true) expect(result.meta.route_used).toBe("graphql") @@ -250,7 +254,7 @@ describe("runGraphqlCapability", () => { ) }) - it("routes pr.review.list through the GraphQL client", async () => { + it("routes pr.reviews.list through the GraphQL client", async () => { const client = { fetchRepoView: vi.fn(), fetchIssueView: vi.fn(), @@ -284,12 +288,16 @@ describe("runGraphqlCapability", () => { submitPrReview: vi.fn(), } - const result = await runGraphqlCapability(client as unknown as GithubClient, "pr.review.list", { - owner: "acme", - name: "modkit", - prNumber: 1, - first: 20, - }) + const result = await runGraphqlCapability( + client as unknown as GithubClient, + "pr.reviews.list", + { + owner: "acme", + name: "modkit", + prNumber: 1, + first: 20, + }, + ) expect(result.ok).toBe(true) expect(result.meta.route_used).toBe("graphql") @@ -391,7 +399,7 @@ describe("runGraphqlCapability", () => { }) }) - it("routes pr.thread.reply through the GraphQL client", async () => { + it("routes pr.threads.reply through the GraphQL client", async () => { const client = { fetchRepoView: vi.fn(), fetchIssueView: vi.fn(), @@ -411,7 +419,7 @@ describe("runGraphqlCapability", () => { const result = await runGraphqlCapability( client as unknown as GithubClient, - "pr.thread.reply", + "pr.threads.reply", { threadId: "thread-1", body: "Thanks, addressed", @@ -422,7 +430,7 @@ describe("runGraphqlCapability", () => { expect(result.data).toEqual({ id: "thread-1", isResolved: false }) }) - it("routes pr.thread.resolve and pr.thread.unresolve through the GraphQL client", async () => { + it("routes pr.threads.resolve and pr.threads.unresolve through the GraphQL client", async () => { const client = { fetchRepoView: vi.fn(), fetchIssueView: vi.fn(), @@ -442,14 +450,14 @@ describe("runGraphqlCapability", () => { const resolveResult = await runGraphqlCapability( client as unknown as GithubClient, - "pr.thread.resolve", + "pr.threads.resolve", { threadId: "thread-1", }, ) const unresolveResult = await runGraphqlCapability( client as unknown as GithubClient, - "pr.thread.unresolve", + "pr.threads.unresolve", { threadId: "thread-1", }, @@ -481,7 +489,7 @@ describe("runGraphqlCapability", () => { const replyResult = await runGraphqlCapability( client as unknown as GithubClient, - "pr.thread.reply", + "pr.threads.reply", { threadId: "", body: "ok", @@ -490,7 +498,7 @@ describe("runGraphqlCapability", () => { const resolveResult = await runGraphqlCapability( client as unknown as GithubClient, - "pr.thread.resolve", + "pr.threads.resolve", { threadId: "", }, @@ -626,7 +634,7 @@ describe("runGraphqlCapability", () => { ) const labelsResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.labels.update", + "issue.labels.set", { issueId: "issue-1", labels: ["bug", "batch-b"], @@ -634,7 +642,7 @@ describe("runGraphqlCapability", () => { ) const assigneesResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.assignees.update", + "issue.assignees.set", { issueId: "issue-1", assignees: ["octocat"], @@ -658,7 +666,7 @@ describe("runGraphqlCapability", () => { ) const linkedPrsResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.linked_prs.list", + "issue.relations.prs.list", { owner: "acme", name: "modkit", @@ -667,7 +675,7 @@ describe("runGraphqlCapability", () => { ) const relationsResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.relations.get", + "issue.relations.view", { owner: "acme", name: "modkit", @@ -676,7 +684,7 @@ describe("runGraphqlCapability", () => { ) const parentSetResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.parent.set", + "issue.relations.parent.set", { issueId: "issue-1", parentIssueId: "issue-parent", @@ -684,14 +692,14 @@ describe("runGraphqlCapability", () => { ) const parentRemoveResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.parent.remove", + "issue.relations.parent.remove", { issueId: "issue-1", }, ) const blockedByAddResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.blocked_by.add", + "issue.relations.blocked_by.add", { issueId: "issue-1", blockedByIssueId: "issue-blocker", @@ -699,7 +707,7 @@ describe("runGraphqlCapability", () => { ) const blockedByRemoveResult = await runGraphqlCapability( client as unknown as GithubClient, - "issue.blocked_by.remove", + "issue.relations.blocked_by.remove", { issueId: "issue-1", blockedByIssueId: "issue-blocker", @@ -819,11 +827,11 @@ describe("runGraphqlCapability", () => { runGraphqlCapability(client as unknown as GithubClient, "issue.delete", { issueId: "issue-1", }), - runGraphqlCapability(client as unknown as GithubClient, "issue.labels.update", { + runGraphqlCapability(client as unknown as GithubClient, "issue.labels.set", { issueId: "issue-1", labels: ["bug"], }), - runGraphqlCapability(client as unknown as GithubClient, "issue.assignees.update", { + runGraphqlCapability(client as unknown as GithubClient, "issue.assignees.set", { issueId: "issue-1", assignees: ["octocat"], }), @@ -835,28 +843,28 @@ describe("runGraphqlCapability", () => { issueId: "issue-1", body: "ack", }), - runGraphqlCapability(client as unknown as GithubClient, "issue.linked_prs.list", { + runGraphqlCapability(client as unknown as GithubClient, "issue.relations.prs.list", { owner: "acme", name: "modkit", issueNumber: 1, }), - runGraphqlCapability(client as unknown as GithubClient, "issue.relations.get", { + runGraphqlCapability(client as unknown as GithubClient, "issue.relations.view", { owner: "acme", name: "modkit", issueNumber: 1, }), - runGraphqlCapability(client as unknown as GithubClient, "issue.parent.set", { + runGraphqlCapability(client as unknown as GithubClient, "issue.relations.parent.set", { issueId: "issue-1", parentIssueId: "issue-2", }), - runGraphqlCapability(client as unknown as GithubClient, "issue.parent.remove", { + runGraphqlCapability(client as unknown as GithubClient, "issue.relations.parent.remove", { issueId: "issue-1", }), - runGraphqlCapability(client as unknown as GithubClient, "issue.blocked_by.add", { + runGraphqlCapability(client as unknown as GithubClient, "issue.relations.blocked_by.add", { issueId: "issue-1", blockedByIssueId: "issue-2", }), - runGraphqlCapability(client as unknown as GithubClient, "issue.blocked_by.remove", { + runGraphqlCapability(client as unknown as GithubClient, "issue.relations.blocked_by.remove", { issueId: "issue-1", blockedByIssueId: "issue-2", }), @@ -1038,7 +1046,7 @@ describe("runGraphqlCapability", () => { const result = await runGraphqlCapability( client as unknown as GithubClient, - "pr.thread.reply", + "pr.threads.reply", { threadId: undefined, body: "comment", @@ -1069,7 +1077,7 @@ describe("runGraphqlCapability", () => { const result = await runGraphqlCapability( client as unknown as GithubClient, - "pr.thread.reply", + "pr.threads.reply", { threadId: "thread-1", body: " ", diff --git a/packages/core/test/unit/operation-cards.test.ts b/packages/core/test/unit/operation-cards.test.ts index 99ca641a..1cce8452 100644 --- a/packages/core/test/unit/operation-cards.test.ts +++ b/packages/core/test/unit/operation-cards.test.ts @@ -16,8 +16,6 @@ describe("operation cards registry", () => { "repo.view", "repo.labels.list", "repo.issue_types.list", - "issue.triage.composite", - "issue.update.composite", "issue.view", "issue.list", "issue.comments.list", @@ -26,71 +24,74 @@ describe("operation cards registry", () => { "issue.close", "issue.reopen", "issue.delete", - "issue.labels.update", + "issue.labels.set", "issue.labels.add", - "issue.assignees.update", + "issue.labels.remove", + "issue.assignees.set", + "issue.assignees.add", + "issue.assignees.remove", "issue.milestone.set", + "issue.milestone.clear", "issue.comments.create", - "issue.linked_prs.list", - "issue.relations.get", - "issue.parent.set", - "issue.parent.remove", - "issue.blocked_by.add", - "issue.blocked_by.remove", - "pr.threads.composite", + "issue.relations.prs.list", + "issue.relations.view", + "issue.relations.parent.set", + "issue.relations.parent.remove", + "issue.relations.blocked_by.add", + "issue.relations.blocked_by.remove", "pr.view", "pr.list", "pr.create", "pr.update", - "pr.thread.list", - "pr.thread.reply", - "pr.thread.resolve", - "pr.thread.unresolve", - "pr.review.list", - "pr.review.request", - "pr.review.submit", + "pr.threads.list", + "pr.threads.reply", + "pr.threads.resolve", + "pr.threads.unresolve", + "pr.reviews.list", + "pr.reviews.request", + "pr.reviews.submit", "pr.diff.files", "pr.diff.view", "pr.checks.list", - "pr.checks.failed", - "pr.checks.rerun_failed", - "pr.checks.rerun_all", + "pr.checks.rerun.failed", + "pr.checks.rerun.all", "pr.merge.status", "pr.merge", - "pr.assignees.update", + "pr.assignees.add", + "pr.assignees.remove", "pr.branch.update", - "check_run.annotations.list", "workflow.list", - "workflow.get", - "project_v2.org.get", - "project_v2.user.get", + "workflow.view", + "project_v2.org.view", + "project_v2.user.view", "project_v2.fields.list", "project_v2.items.list", - "project_v2.item.add_issue", - "project_v2.item.field.update", + "project_v2.items.issue.add", + "project_v2.items.issue.remove", + "project_v2.items.field.update", "release.list", - "release.get", - "release.create_draft", + "release.view", + "release.create", "release.update", - "release.publish_draft", - "workflow.dispatch.run", - "workflow.job.logs.get", + "release.publish", + "workflow.dispatch", + "workflow.job.logs.view", "workflow.job.logs.raw", "workflow.run.artifacts.list", "workflow.run.cancel", - "workflow.run.rerun_all", - "workflow.run.rerun_failed", + "workflow.run.rerun.all", + "workflow.run.rerun.failed", "workflow.run.view", "workflow.runs.list", ]) }) it("marks release and delivery batch cards as CLI-preferred", () => { - const releaseCreateDraft = getOperationCard("release.create_draft") - const releasePublishDraft = getOperationCard("release.publish_draft") - const workflowDispatchRun = getOperationCard("workflow.dispatch.run") - const workflowRunRerunFailed = getOperationCard("workflow.run.rerun_failed") + const releaseCreateDraft = getOperationCard("release.create") + const releasePublishDraft = getOperationCard("release.publish") + const workflowDispatchRun = getOperationCard("workflow.dispatch") + const workflowRunRerunFailed = getOperationCard("workflow.run.rerun.failed") expect(releaseCreateDraft?.routing.preferred).toBe("cli") expect(releasePublishDraft?.routing.preferred).toBe("cli") @@ -99,12 +100,12 @@ describe("operation cards registry", () => { }) it("marks Projects v2 and repo issue types as CLI-preferred with no fallbacks", () => { - const projectOrg = getOperationCard("project_v2.org.get") - const projectUser = getOperationCard("project_v2.user.get") + const projectOrg = getOperationCard("project_v2.org.view") + const projectUser = getOperationCard("project_v2.user.view") const projectFields = getOperationCard("project_v2.fields.list") const projectItems = getOperationCard("project_v2.items.list") - const projectItemAddIssue = getOperationCard("project_v2.item.add_issue") - const projectItemFieldUpdate = getOperationCard("project_v2.item.field.update") + const projectItemAddIssue = getOperationCard("project_v2.items.issue.add") + const projectItemFieldUpdate = getOperationCard("project_v2.items.field.update") const issueTypes = getOperationCard("repo.issue_types.list") expect(projectOrg?.routing.preferred).toBe("cli") @@ -140,15 +141,15 @@ describe("operation cards registry", () => { it("requires explicit pagination input for list capabilities", () => { const issueListCard = getOperationCard("issue.list") const prListCard = getOperationCard("pr.list") - const prThreadCard = getOperationCard("pr.thread.list") + const prThreadCard = getOperationCard("pr.threads.list") expect(issueListCard?.input_schema.required).toEqual(["owner", "name"]) expect(prListCard?.input_schema.required).toEqual(["owner", "name"]) expect(prThreadCard?.input_schema.required).toEqual(["owner", "name", "prNumber"]) }) - it("supports unresolved and outdated filters for pr.thread.list", () => { - const card = getOperationCard("pr.thread.list") + it("supports unresolved and outdated filters for pr.threads.list", () => { + const card = getOperationCard("pr.threads.list") const properties = card?.input_schema.properties as Record expect(properties.unresolvedOnly).toEqual({ type: "boolean" }) @@ -226,10 +227,11 @@ describe("operation cards registry", () => { "pr.merge", "pr.create", "pr.update", - "pr.checks.rerun_failed", - "pr.checks.rerun_all", - "pr.review.request", - "pr.assignees.update", + "pr.checks.rerun.failed", + "pr.checks.rerun.all", + "pr.reviews.request", + "pr.assignees.add", + "pr.assignees.remove", "pr.branch.update", ] @@ -243,8 +245,8 @@ describe("operation cards registry", () => { } }) - it("pr.review.submit prefers GraphQL for inline comments support", () => { - const card = getOperationCard("pr.review.submit") + it("pr.reviews.submit prefers GraphQL for inline comments support", () => { + const card = getOperationCard("pr.reviews.submit") expect(card).toBeDefined() expect(card?.routing.preferred).toBe("graphql") expect(card?.routing.fallbacks).toEqual([]) diff --git a/packages/core/test/unit/pr-review-submit-engine.test.ts b/packages/core/test/unit/pr-review-submit-engine.test.ts index 4a0a54bd..1db475aa 100644 --- a/packages/core/test/unit/pr-review-submit-engine.test.ts +++ b/packages/core/test/unit/pr-review-submit-engine.test.ts @@ -2,7 +2,7 @@ import { executeTask } from "@core/core/routing/engine.js" import type { GithubClient } from "@core/gql/github-client.js" import { describe, expect, it, vi } from "vitest" -describe("pr.review.submit executeTask contract", () => { +describe("pr.reviews.submit executeTask contract", () => { it("returns GraphQL review payload shape from engine", async () => { const githubClient = { fetchRepoView: vi.fn(), @@ -29,7 +29,7 @@ describe("pr.review.submit executeTask contract", () => { const result = await executeTask( { - task: "pr.review.submit", + task: "pr.reviews.submit", input: { owner: "owner", name: "repo", diff --git a/packages/core/test/unit/pr-review-submit-graphql.test.ts b/packages/core/test/unit/pr-review-submit-graphql.test.ts index 8d4be45c..c15a1bd8 100644 --- a/packages/core/test/unit/pr-review-submit-graphql.test.ts +++ b/packages/core/test/unit/pr-review-submit-graphql.test.ts @@ -2,7 +2,7 @@ import { runGraphqlCapability } from "@core/core/execution/adapters/graphql-capa import type { GithubClient } from "@core/gql/github-client.js" import { beforeEach, describe, expect, it, vi } from "vitest" -describe("pr.review.submit via GraphQL", () => { +describe("pr.reviews.submit via GraphQL", () => { let client: Pick< GithubClient, | "fetchRepoView" @@ -48,7 +48,7 @@ describe("pr.review.submit via GraphQL", () => { it("submits review with inline comments using addPullRequestReview mutation", async () => { const result = await runGraphqlCapability( client as unknown as GithubClient, - "pr.review.submit", + "pr.reviews.submit", { owner: "owner", name: "repo", @@ -67,7 +67,7 @@ describe("pr.review.submit via GraphQL", () => { it("submits review without comments (body-only)", async () => { const result = await runGraphqlCapability( client as unknown as GithubClient, - "pr.review.submit", + "pr.reviews.submit", { owner: "owner", name: "repo", diff --git a/packages/core/test/unit/registry-validation.test.ts b/packages/core/test/unit/registry-validation.test.ts index 552421ce..8263cd61 100644 --- a/packages/core/test/unit/registry-validation.test.ts +++ b/packages/core/test/unit/registry-validation.test.ts @@ -54,30 +54,11 @@ describe("validateOperationCard", () => { expect(result.ok).toBe(true) }) - it("accepts card with valid composite config", () => { - const card = { - ...validBaseCard, - capability_id: "pr.threads.composite", - composite: { - steps: [ - { - capability_id: "pr.thread.reply", - foreach: "threads", - params_map: { threadId: "threadId" }, - }, - ], - output_strategy: "array", - }, - } - const result = validateOperationCard(card) - expect(result.ok).toBe(true) - }) - it("rejects card with invalid output_strategy", () => { const card = { ...validBaseCard, composite: { - steps: [{ capability_id: "pr.thread.reply", params_map: {} }], + steps: [{ capability_id: "pr.threads.reply", params_map: {} }], output_strategy: "invalid", }, }