Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 127 additions & 1 deletion .github/scripts/__tests__/keepalive-reporter-applicability.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const { classifyReporterRun } = require('../keepalive_reporter_applicability.js');
const {
classifyReporterRun,
recoverReporterAuthority,
} = require('../keepalive_reporter_applicability.js');
const { formatStateComment, loadKeepaliveState } = require('../keepalive_state.js');

const root = path.resolve(__dirname, '../../..');
const head = 'a'.repeat(40);
Expand Down Expand Up @@ -34,6 +38,37 @@ function setup(source, title, overrides = {}) {
return { github, reads: () => reads };
}

function recoveryGithub(state) {
const comments = [{
id: 91,
body: '<!-- keepalive-loop-summary -->\n' + formatStateComment(state),
html_url: 'https://example.com/91',
user: { login: 'agents-workflows-bot[bot]', type: 'Bot' },
}];
const actions = [];
return {
actions,
rest: {
issues: {
listComments: async () => ({ data: comments }),
getComment: async ({ comment_id: commentId }) => ({
data: comments.find((comment) => comment.id === commentId),
}),
updateComment: async ({ comment_id: commentId, body }) => {
comments.find((comment) => comment.id === commentId).body = body;
actions.push({ type: 'update', commentId, body });
return { data: { id: commentId } };
},
createComment: async ({ body }) => {
actions.push({ type: 'create', body });
return { data: { id: 92, html_url: 'https://example.com/92' } };
},
},
},
paginate: async (fn, params) => (await fn(params)).data,
};
}

for (const source of sources) {
test(`${source}: ordinary unassociated dispatch skips only after absent index`, async () => {
const { github, reads } = setup(source, 'keepalive-dispatch/v1 ordinary');
Expand Down Expand Up @@ -98,3 +133,94 @@ test('associated runs avoid the unassociated locator altogether', async () => {
lookupTarget: async () => { throw new Error('unexpected lookup'); } });
assert.deepEqual(result, { status: 'continue' });
});

for (const status of ['released', 'reopened']) {
test(`delayed ${status} recovery is located, reconciled, and projected after summary retry`, async () => {
const previousGeneration = 'b'.repeat(64);
const nextGeneration = 'c'.repeat(64);
const ownerAttempt = 'stranske/repo:12345:2';
const github = recoveryGithub({
running: false,
attention: {
owner: 'automation', disposition: 'automation-retry', generation: '',
boundary_fingerprint: '', challenge_due_at: null, expires_at: '',
recovery_generation: previousGeneration, recovery_owner_attempt: ownerAttempt,
},
});
const receiptName = status === 'released' ? 'released_receipt' : 'recovered_receipt';
const recovery = {
status,
previousGeneration,
state: {
generation: nextGeneration,
boundary_fingerprint: 'd'.repeat(64),
[receiptName]: { owner_attempt: ownerAttempt },
due_at: '2026-09-30T01:00:00Z',
expires_at: '2026-09-30T13:00:00Z',
},
};
const context = { repo: { owner: 'stranske', repo: 'repo' } };
const args = {
github, context, run, workerEvidence: 'not-started',
writerLogin: 'agents-workflows-bot[bot]',
makeRequest: () => 'request',
lookupTarget: async ({ repository, ownerAttempt: attempt }) => {
assert.equal(repository, 'stranske/repo');
assert.equal(attempt, ownerAttempt);
return { prNumber: 42 };
},
reconcileAttempt: async (input) => {
assert.equal(input.request, 'request');
assert.equal(input.ownerAttempt, ownerAttempt);
assert.equal(input.prNumber, 42);
return recovery;
},
};
const first = await recoverReporterAuthority(args);
assert.equal(first.status, 'projected');
assert.equal(first.projection.reason, 'recovered-summary-projected');
const second = await recoverReporterAuthority(args);
assert.equal(second.projection.reason, 'already-projected');
assert.equal(github.actions.filter((action) => action.type === 'update').length, 1);
const loaded = await loadKeepaliveState({ github, context, prNumber: 42, trace: '' });
assert.equal(loaded.state.attention.disposition, 'challenge-due');
assert.equal(loaded.state.attention.generation, nextGeneration);
assert.equal(loaded.state.attention.recovery_owner_attempt, ownerAttempt);
});
}

test('recovery rejects unknown worker evidence before authority mutation', async () => {
await assert.rejects(recoverReporterAuthority({
github: {}, context: { repo: { owner: 'stranske', repo: 'repo' } }, run,
workerEvidence: 'unknown', writerLogin: 'agents-workflows-bot[bot]',
makeRequest: () => { throw new Error('must not request'); },
}), /execution evidence is unknown/);
});

test('recovery rejects a delayed summary marker for a different attempt or generation', async () => {
const github = recoveryGithub({
running: false,
attention: {
owner: 'automation', disposition: 'automation-retry', generation: '',
recovery_generation: 'e'.repeat(64),
recovery_owner_attempt: 'stranske/repo:999:1',
},
});
await assert.rejects(recoverReporterAuthority({
github,
context: { repo: { owner: 'stranske', repo: 'repo' } },
run,
workerEvidence: 'not-started',
writerLogin: 'agents-workflows-bot[bot]',
makeRequest: () => 'request',
lookupTarget: async () => ({ prNumber: 42 }),
reconcileAttempt: async () => ({
status: 'released', previousGeneration: 'b'.repeat(64), state: {
generation: 'c'.repeat(64), boundary_fingerprint: 'd'.repeat(64),
released_receipt: { owner_attempt: 'stranske/repo:12345:2' },
due_at: '2026-09-30T01:00:00Z', expires_at: '2026-09-30T13:00:00Z',
},
}),
}), /does not match the recovered generation/);
assert.equal(github.actions.length, 0);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
59 changes: 57 additions & 2 deletions .github/scripts/keepalive_reporter_applicability.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
'use strict';

const { findAuthorityPrForAttempt, requester } = require('./keepalive_authority_state.js');
const {
findAuthorityPrForAttempt,
reconcileFailedAuthorityAttempt,
requester,
} = require('./keepalive_authority_state.js');
const { projectRecoveredAuthorityState } = require('./keepalive_state.js');
const { withRetry } = require('./github-api-with-retry.js');

const DISPATCH_TITLE = 'keepalive-dispatch/v1 ';
Expand Down Expand Up @@ -47,4 +52,54 @@ async function classifyReporterRun({ github, owner, repo, run, lookupTarget = fi
return { status: 'skip' };
}

module.exports = { classifyReporterRun };
async function recoverReporterAuthority({
github,
context,
run,
workerEvidence,
writerLogin,
prNumber = Number(run.pull_requests?.[0]?.number || 0),
lookupTarget = findAuthorityPrForAttempt,
reconcileAttempt = reconcileFailedAuthorityAttempt,
projectRecovery = projectRecoveredAuthorityState,
makeRequest = requester,
}) {
if (!['started', 'not-started'].includes(workerEvidence)) {
throw new Error('Originating worker execution evidence is unknown');
}
const owner = context.repo.owner;
const repo = context.repo.repo;
const repository = `${owner}/${repo}`;
const ownerAttempt = `${repository}:${run.id}:${run.run_attempt || 1}`.toLowerCase();
const request = makeRequest(github);
let authorityTarget;
if (!prNumber) {
authorityTarget = await lookupTarget({ request, repository, ownerAttempt });
prNumber = Number(authorityTarget?.prNumber || 0);
}
if (!prNumber) {
throw new Error('No PR association or authoritative attempt target for failed run');
}
let reconciliation;
try {
reconciliation = await reconcileAttempt({
request, repository, prNumber, ownerAttempt, workerEvidence,
});
} catch (error) {
if (error.status === 404) {
return { status: 'continue', prNumber, ownerAttempt, authorityTarget };
}
throw error;
}
if (['released', 'reopened'].includes(reconciliation.status)) {
const projection = await projectRecovery({
github, context, prNumber, recovery: reconciliation, writerLogin,
});
return {
status: 'projected', prNumber, ownerAttempt, authorityTarget, reconciliation, projection,
};
}
return { status: 'continue', prNumber, ownerAttempt, authorityTarget, reconciliation };
}

module.exports = { classifyReporterRun, recoverReporterAuthority };
59 changes: 18 additions & 41 deletions .github/workflows/agents-keepalive-loop-reporter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,11 @@ jobs:
return;
}

const { loadKeepaliveState, projectRecoveredAuthorityState } =
require('./.github/scripts/keepalive_state.js');
const { loadKeepaliveState } = require('./.github/scripts/keepalive_state.js');
const { updateKeepaliveLoopSummary } = require('./.github/scripts/keepalive_loop.js');
const { getWorkerExecutionEvidence } = require('./.github/scripts/keepalive_worker_evidence.js');
const { recoverReporterAuthority } =
require('./.github/scripts/keepalive_reporter_applicability.js');

const workerEvidence = await getWorkerExecutionEvidence(
github, context.repo.owner, context.repo.repo, run.id, run.run_attempt || 1,
Expand All @@ -187,48 +188,24 @@ jobs:
core.setFailed('Originating worker execution evidence is unknown; retry this reporter');
return;
}
const ownerAttempt =
`${context.repo.owner}/${context.repo.repo}:${run.id}:${run.run_attempt || 1}`.toLowerCase();
const { findAuthorityPrForAttempt, reconcileFailedAuthorityAttempt, requester } =
require('./.github/scripts/keepalive_authority_state.js');
const authorityRequest = requester(github);
let authorityTarget;
if (!prNumber) {
try {
authorityTarget = await findAuthorityPrForAttempt({
request: authorityRequest,
repository: `${context.repo.owner}/${context.repo.repo}`,
ownerAttempt,
});
} catch (error) {
core.setFailed(`Authority attempt target lookup unavailable: ${error.message}`);
return;
}
}
prNumber ||= authorityTarget?.prNumber || 0;
if (!prNumber) {
core.setFailed('No PR association or authoritative attempt target for failed run');
return;
}
let authorityRecovery;
try {
const reconciliation = await reconcileFailedAuthorityAttempt({
request: authorityRequest,
repository: `${context.repo.owner}/${context.repo.repo}`,
prNumber, ownerAttempt, workerEvidence,
authorityRecovery = await recoverReporterAuthority({
github, context, run, workerEvidence,
writerLogin: process.env.KEEPALIVE_SUMMARY_WRITER || '', prNumber,
});
core.info(`Attempt-bound authority reconciliation: ${reconciliation.status}`);
if (['released', 'reopened'].includes(reconciliation.status)) {
await projectRecoveredAuthorityState({
github, context, prNumber, recovery: reconciliation,
writerLogin: process.env.KEEPALIVE_SUMMARY_WRITER || '',
});
return;
}
} catch (error) {
if (error.status !== 404) {
core.setFailed(`Authority reconciliation unavailable: ${error.message}`);
return;
}
core.setFailed(`Authority reconciliation unavailable: ${error.message}`);
return;
}
prNumber = authorityRecovery.prNumber;
const authorityTarget = authorityRecovery.authorityTarget;
Comment thread
stranske marked this conversation as resolved.
Resolved
Comment thread
stranske marked this conversation as resolved.
Resolved
core.info(
`Attempt-bound authority reconciliation: ` +
`${authorityRecovery.reconciliation?.status || authorityRecovery.status}`
);
if (authorityRecovery.status === 'projected') {
return;
}

const { state } = await loadKeepaliveState({ github, context, prNumber, trace: '' });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
'use strict';

const { findAuthorityPrForAttempt, requester } = require('./keepalive_authority_state.js');
const {
findAuthorityPrForAttempt,
reconcileFailedAuthorityAttempt,
requester,
} = require('./keepalive_authority_state.js');
const { projectRecoveredAuthorityState } = require('./keepalive_state.js');
const { withRetry } = require('./github-api-with-retry.js');

const DISPATCH_TITLE = 'keepalive-dispatch/v1 ';
Expand Down Expand Up @@ -47,4 +52,54 @@ async function classifyReporterRun({ github, owner, repo, run, lookupTarget = fi
return { status: 'skip' };
}

module.exports = { classifyReporterRun };
async function recoverReporterAuthority({
github,
context,
run,
workerEvidence,
writerLogin,
prNumber = Number(run.pull_requests?.[0]?.number || 0),
lookupTarget = findAuthorityPrForAttempt,
reconcileAttempt = reconcileFailedAuthorityAttempt,
projectRecovery = projectRecoveredAuthorityState,
makeRequest = requester,
}) {
if (!['started', 'not-started'].includes(workerEvidence)) {
throw new Error('Originating worker execution evidence is unknown');
}
const owner = context.repo.owner;
const repo = context.repo.repo;
const repository = `${owner}/${repo}`;
const ownerAttempt = `${repository}:${run.id}:${run.run_attempt || 1}`.toLowerCase();
const request = makeRequest(github);
let authorityTarget;
if (!prNumber) {
authorityTarget = await lookupTarget({ request, repository, ownerAttempt });
prNumber = Number(authorityTarget?.prNumber || 0);
}
if (!prNumber) {
throw new Error('No PR association or authoritative attempt target for failed run');
}
let reconciliation;
try {
reconciliation = await reconcileAttempt({
request, repository, prNumber, ownerAttempt, workerEvidence,
});
} catch (error) {
if (error.status === 404) {
return { status: 'continue', prNumber, ownerAttempt, authorityTarget };
}
throw error;
}
if (['released', 'reopened'].includes(reconciliation.status)) {
const projection = await projectRecovery({
github, context, prNumber, recovery: reconciliation, writerLogin,
});
return {
status: 'projected', prNumber, ownerAttempt, authorityTarget, reconciliation, projection,
};
}
return { status: 'continue', prNumber, ownerAttempt, authorityTarget, reconciliation };
}

module.exports = { classifyReporterRun, recoverReporterAuthority };
Loading
Loading