diff --git a/.github/scripts/__tests__/keepalive-orchestrator-gate-runner.test.js b/.github/scripts/__tests__/keepalive-orchestrator-gate-runner.test.js index c37cbd72f..a7164668d 100644 --- a/.github/scripts/__tests__/keepalive-orchestrator-gate-runner.test.js +++ b/.github/scripts/__tests__/keepalive-orchestrator-gate-runner.test.js @@ -69,6 +69,7 @@ function createGithub(options = {}) { combinedStatus = { state: 'success', statuses: [] }, comments = [], graphqlError, + graphqlUnavailable = false, } = options; const calls = { labelAdds: [], @@ -113,15 +114,17 @@ function createGithub(options = {}) { async paginate() { return comments; }, - async graphql(query, variables) { + __calls: calls, + }; + if (!graphqlUnavailable) { + github.graphql = async function graphql(query, variables) { calls.graphql.push({ query, variables }); if (graphqlError) { throw graphqlError; } return { markPullRequestReadyForReview: { pullRequest: { number: pull?.number || 17, isDraft: false } } }; - }, - __calls: calls, - }; + }; + } return github; } @@ -435,10 +438,55 @@ test('runKeepaliveGate converts checklist-complete draft PRs to ready for review assert.equal(outputs.reason, ''); assert.equal(github.__calls.graphql.length, 1); assert.equal(github.__calls.graphql[0].variables.pullRequestId, 'PR_node_17'); + const readyMutation = github.__calls.graphql[0].query; + assert.match(readyMutation, /mutation MarkPullRequestReadyForReview\(\$pullRequestId: ID!\) \{/); + assert.equal( + (readyMutation.match(/\{/g) || []).length, + (readyMutation.match(/\}/g) || []).length + ); + assert.match(readyMutation, /\n\}$/); assert.ok(summary.entries.some((entry) => entry.text?.includes('marked ready for review'))); restore(); }); +test('runKeepaliveGate distinguishes unavailable GraphQL client from missing PR node id', async () => { + const { core, outputs, summary } = createCore(); + const gateStub = async () => createGateResult(); + const { runKeepaliveGate, restore } = loadRunnerWithGate(gateStub); + + const pr = makePullRequest({ + draft: true, + labels: ['agents:keepalive', 'agent:codex'], + body: '- [x] Implementation complete\n- [x] Verified behavior', + }); + const github = createGithub({ + pull: pr, + graphqlUnavailable: true, + }); + + await runKeepaliveGate({ + core, + github, + context: { repo: { owner: 'octo', repo: 'demo' }, runId: 44 }, + env: makeEnv({ KEEPALIVE_MAX_RETRIES: '5' }), + }); + + assert.equal(outputs.proceed, 'false'); + assert.equal(outputs.reason, 'pr-draft-ready-failed'); + assert.equal(github.__calls.graphql.length, 0); + assert.ok( + summary.entries.some((entry) => + entry.text?.includes('GitHub GraphQL client is unavailable') + ) + ); + assert.ok( + !summary.entries.some((entry) => + entry.text?.includes('missing GraphQL PR node id') + ) + ); + restore(); +}); + test('runKeepaliveGate routes incomplete draft PRs to human', async () => { const { core, outputs } = createCore(); const gateStub = async () => createGateResult(); diff --git a/.github/scripts/keepalive_orchestrator_gate_runner.js b/.github/scripts/keepalive_orchestrator_gate_runner.js index 2667f8be0..498edace8 100644 --- a/.github/scripts/keepalive_orchestrator_gate_runner.js +++ b/.github/scripts/keepalive_orchestrator_gate_runner.js @@ -14,6 +14,14 @@ const PAUSE_LABEL = 'agents:paused'; const NEEDS_HUMAN_LABEL = 'needs-human'; const NEEDS_ATTENTION_LABEL = 'agent:needs-attention'; const DRAFT_DISPOSITION_MARKER = ''; +const MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION = `mutation MarkPullRequestReadyForReview($pullRequestId: ID!) { + markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { + pullRequest { + number + isDraft + } + } +}`; function normaliseLabelName(label) { if (!label) { @@ -105,23 +113,17 @@ async function addLabelsIfMissing({ github, owner, repo, prNumber, labels, curre async function markDraftReadyForReview({ github, pr, core, summary }) { const nodeId = String(pr?.node_id || '').trim(); - if (!nodeId || typeof github.graphql !== 'function') { + if (!nodeId) { summary.addRaw('Draft PR could not be converted automatically: missing GraphQL PR node id.').addEOL(); return false; } + if (typeof github.graphql !== 'function') { + summary.addRaw('Draft PR could not be converted automatically: GitHub GraphQL client is unavailable.').addEOL(); + return false; + } try { - await github.graphql( - `mutation($pullRequestId: ID!) { - markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { - pullRequest { - number - isDraft - } - } - }`, - { pullRequestId: nodeId } - ); + await github.graphql(MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION, { pullRequestId: nodeId }); summary.addRaw('Draft PR had no unchecked checklist items; marked ready for review.').addEOL(); return true; } catch (error) { diff --git a/templates/consumer-repo/.github/scripts/keepalive_orchestrator_gate_runner.js b/templates/consumer-repo/.github/scripts/keepalive_orchestrator_gate_runner.js index 2667f8be0..498edace8 100644 --- a/templates/consumer-repo/.github/scripts/keepalive_orchestrator_gate_runner.js +++ b/templates/consumer-repo/.github/scripts/keepalive_orchestrator_gate_runner.js @@ -14,6 +14,14 @@ const PAUSE_LABEL = 'agents:paused'; const NEEDS_HUMAN_LABEL = 'needs-human'; const NEEDS_ATTENTION_LABEL = 'agent:needs-attention'; const DRAFT_DISPOSITION_MARKER = ''; +const MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION = `mutation MarkPullRequestReadyForReview($pullRequestId: ID!) { + markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { + pullRequest { + number + isDraft + } + } +}`; function normaliseLabelName(label) { if (!label) { @@ -105,23 +113,17 @@ async function addLabelsIfMissing({ github, owner, repo, prNumber, labels, curre async function markDraftReadyForReview({ github, pr, core, summary }) { const nodeId = String(pr?.node_id || '').trim(); - if (!nodeId || typeof github.graphql !== 'function') { + if (!nodeId) { summary.addRaw('Draft PR could not be converted automatically: missing GraphQL PR node id.').addEOL(); return false; } + if (typeof github.graphql !== 'function') { + summary.addRaw('Draft PR could not be converted automatically: GitHub GraphQL client is unavailable.').addEOL(); + return false; + } try { - await github.graphql( - `mutation($pullRequestId: ID!) { - markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { - pullRequest { - number - isDraft - } - } - }`, - { pullRequestId: nodeId } - ); + await github.graphql(MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION, { pullRequestId: nodeId }); summary.addRaw('Draft PR had no unchecked checklist items; marked ready for review.').addEOL(); return true; } catch (error) {