const assert = require('node:assert/strict'); const { test } = require('node:test'); const { prepare, complete, targetArtifactName, assessmentSucceeded } = require('./issue-assessment.cjs'); function fixture(type = 'Issue') { const updatedAt = '2026-09-07T12:00:00Z'; const node = { id: 'subject', __typename: type, updatedAt, repository: { nameWithOwner: 'crmne/spotifast' }, reactionGroups: [{ content: 'ROCKET', viewerHasReacted: false }], }; const mutations = []; const context = { repo: { owner: 'crmne', repo: 'spotifast' }, eventName: 'issues', payload: { issue: { number: 350, node_id: 'subject' }, sender: { type: 'User', login: 'reporter' }, repository: { default_branch: 'main' }, workflow_run: { conclusion: 'success', path: '.github/workflows/issue-assessment.lock.yml', head_branch: 'main', head_repository: { full_name: 'crmne/spotifast' }, }, }, }; if (type === 'Discussion' || type === 'DiscussionComment') { delete context.payload.issue; context.payload.discussion = { number: 350, node_id: 'subject' }; context.eventName = 'discussion'; } if (type.endsWith('Comment')) { context.eventName = type === 'IssueComment' ? 'issue_comment' : 'discussion_comment'; context.payload.comment = { node_id: 'subject', updated_at: updatedAt, user: { type: 'User' } }; } if (type === 'DiscussionComment') { node.discussion = { repository: node.repository }; delete node.repository; } const github = { graphql: async (query, variables) => { if (query.startsWith('mutation')) { mutations.push({ operation: query.includes('removeReaction') ? 'remove' : 'add', ...variables }); return {}; } return { node }; }, }; return { github, context, node, mutations }; } for (const type of ['Issue', 'Discussion', 'IssueComment', 'DiscussionComment']) { test(`${type}: successful assessment marks the triggering subject only after completion`, async () => { const f = fixture(type); const target = await prepare(f); assert.equal(target.subjectId, 'subject'); assert.deepEqual(f.mutations, []); await complete({ ...f, target }); assert.deepEqual(f.mutations, [{ operation: 'add', subjectId: 'subject' }]); }); } test('an existing bot rocket is cleared and never blocks reassessment or retry', async () => { const f = fixture(); f.node.reactionGroups[0].viewerHasReacted = true; const target = await prepare(f); assert.ok(target); assert.deepEqual(f.mutations, [{ operation: 'remove', subjectId: 'subject' }]); f.context.payload.workflow_run.conclusion = 'failure'; await complete({ ...f, target }); assert.equal(f.mutations.length, 1); assert.ok(await prepare(f)); }); test('maintainer and reporter reactions are preserved', async () => { const f = fixture(); assert.ok(await prepare(f)); assert.deepEqual(f.mutations, []); }); for (const conclusion of ['failure', 'cancelled', 'timed_out', 'skipped', 'action_required']) { test(`${conclusion} does not get a completion marker`, async () => { const f = fixture(); const target = await prepare(f); f.context.payload.workflow_run.conclusion = conclusion; await complete({ ...f, target }); assert.deepEqual(f.mutations, []); }); } for (const user of [{ type: 'Bot', login: 'github-actions' }, { login: 'github-actions[bot]' }]) { test(`bot comments cannot start a loop (${user.login})`, async () => { const f = fixture('IssueComment'); f.context.payload.comment.user = user; assert.equal(await prepare(f), null); assert.deepEqual(f.mutations, []); }); } test('bot edits and pull request comments are filtered', async () => { const f = fixture('IssueComment'); f.context.payload.sender.type = 'Bot'; assert.equal(await prepare(f), null); f.context.payload.sender.type = 'User'; f.context.payload.issue.pull_request = {}; assert.equal(await prepare(f), null); assert.deepEqual(f.mutations, []); }); function workflowFailure() { return { number: 428, node_id: 'alert', user: { type: 'Bot', login: 'github-actions[bot]' }, title: '[aw] Failed jobs: Copilot issue assessment', labels: [{ name: 'agentic-workflows' }, { name: 'duplicate' }], }; } test('human comments and reopenings cannot reassess workflow failure alerts', async () => { const engineFailure = { ...workflowFailure(), title: '[aw] Copilot issue assessment hit engine rate limit (HTTP 429)', body: '', }; for (const issue of [workflowFailure(), engineFailure]) { for (const [type, action] of [ ['IssueComment', 'created'], ['IssueComment', 'edited'], ['Issue', 'reopened'], ]) { const f = fixture(type); f.context.payload.action = action; f.context.payload.issue = issue; f.context.payload.sender = { type: 'User', login: 'crmne' }; f.node.reactionGroups[0].viewerHasReacted = true; f.github.graphql = async () => { throw new Error('alert must be filtered before any reaction API'); }; assert.equal(await prepare(f), null); assert.deepEqual(f.mutations, []); } } }); test('manual dispatch cannot feed a workflow failure alert back into assessment', async () => { const f = fixture(); f.context.eventName = 'workflow_dispatch'; delete f.context.payload.issue; f.context.payload.inputs = { aw_context: JSON.stringify({ item_type: 'issue', item_number: 428 }) }; f.github.rest = { issues: { get: async args => { assert.equal(args.issue_number, 428); return { data: workflowFailure() }; } } }; f.github.graphql = async () => { throw new Error('alert must be filtered before any reaction API'); }; assert.equal(await prepare(f), null); assert.deepEqual(f.mutations, []); }); test('ordinary maintainer conversations and human reports about workflows still get assessed', async () => { for (const change of [ { user: { type: 'User', login: 'reporter' }, body: '' }, { labels: [] }, { title: 'Playback stops after an hour' }, ]) { const f = fixture('IssueComment'); f.context.payload.issue = { ...workflowFailure(), ...change }; f.context.payload.sender = { type: 'User', login: 'crmne' }; assert.ok(await prepare(f)); assert.deepEqual(f.mutations, []); } }); for (const type of ['IssueComment', 'DiscussionComment']) { test(`${type}: an edit during assessment cannot be marked by the older run`, async () => { const f = fixture(type); const target = await prepare(f); f.node.updatedAt = '2026-09-07T12:01:00Z'; await complete({ ...f, target }); assert.deepEqual(f.mutations, []); assert.equal(await prepare(f), null); f.context.payload.comment.updated_at = f.node.updatedAt; const edited = await prepare(f); await complete({ ...f, target: edited }); assert.equal(f.mutations.length, 1); }); } test('completion accepts only this workflow on the default branch of this repository', async () => { for (const change of [ { path: '.github/workflows/other.yml' }, { head_branch: 'contributor' }, { head_repository: { full_name: 'someone/spotifast' } }, ]) { const f = fixture(); Object.assign(f.context.payload.workflow_run, change); await complete({ ...f, target: { subjectId: 'subject' } }); assert.deepEqual(f.mutations, []); } }); test('artifact data cannot direct a reaction outside the repository', async () => { const f = fixture(); f.node.repository.nameWithOwner = 'someone/else'; await assert.rejects(complete({ ...f, target: { subjectId: 'subject' } }), /this repository/); assert.deepEqual(f.mutations, []); }); test('missing or deleted subjects cannot receive a completion marker', async () => { const f = fixture(); await assert.rejects(prepare({ ...f, context: { ...f.context, payload: {} } }), /subject is required/); f.github.graphql = async () => ({ node: null }); assert.equal(await prepare(f), null); await complete({ ...f, target: { subjectId: 'deleted' } }); assert.deepEqual(f.mutations, []); }); test('manual issue assessment resolves the issue and rejects pull requests', async () => { const f = fixture(); f.context.eventName = 'workflow_dispatch'; delete f.context.payload.issue; f.context.payload.inputs = { aw_context: JSON.stringify({ item_type: 'issue', item_number: 350 }) }; const data = { node_id: 'subject' }; f.github.rest = { issues: { get: async args => { assert.equal(args.issue_number, 350); return { data }; } } }; assert.ok(await prepare(f)); data.pull_request = {}; assert.equal(await prepare(f), null); }); test('manual discussion assessment resolves the discussion', async () => { const f = fixture('Discussion'); f.context.eventName = 'workflow_dispatch'; delete f.context.payload.discussion; f.context.payload.inputs = { aw_context: JSON.stringify({ item_type: 'discussion', item_number: 350 }) }; const graphql = f.github.graphql; f.github.graphql = async (query, args) => args.number ? { repository: { discussion: { id: 'subject' } } } : graphql(query, args); assert.ok(await prepare(f)); }); test('invalid manual context and API failures fail preparation instead of looking assessed', async () => { const f = fixture(); f.context.eventName = 'workflow_dispatch'; delete f.context.payload.issue; for (const aw_context of ['{', '{}', '{"item_type":"issue","item_number":-1}']) { f.context.payload.inputs = { aw_context }; await assert.rejects(prepare(f)); } const failing = fixture(); failing.github.graphql = async () => { throw new Error('API unavailable'); }; await assert.rejects(prepare(failing), /API unavailable/); assert.deepEqual(failing.mutations, []); }); test('rerunning failed jobs can mark the assessment prepared by an earlier attempt', () => { const artifacts = [{ name: 'assessment-target-1', expired: false }]; assert.equal(targetArtifactName(artifacts, 2), 'assessment-target-1'); artifacts.push({ name: 'assessment-target-2', expired: false }); assert.equal(targetArtifactName(artifacts, 2), 'assessment-target-2'); artifacts.push({ name: 'assessment-target-10', expired: false }); assert.equal(targetArtifactName(artifacts, 10), 'assessment-target-10'); assert.equal(targetArtifactName(artifacts, 1), 'assessment-target-1'); }); test('filtered events, expired artifacts, and unrelated artifacts have no completion target', () => { assert.equal(targetArtifactName([], 1), ''); assert.equal(targetArtifactName([ { name: 'assessment-target-1', expired: true }, { name: 'agent', expired: false }, { name: 'assessment-target-2', expired: false }, ], 1), ''); }); function completedJobs() { return { agent: { result: 'success', outputs: { output_types: '' } }, detection: { result: 'success' }, safe_outputs: { result: 'success', outputs: { process_safe_outputs_items_failed: '0', process_safe_outputs_processed_count: '0', } }, }; } test('successful jobs with no safe output do not count as an assessment', () => { assert.equal(assessmentSucceeded(completedJobs()), false); assert.equal(assessmentSucceeded({}), false); }); test('an explicit no-action result or an applied action completes assessment', () => { const jobs = completedJobs(); jobs.agent.outputs.output_types = 'noop'; assert.equal(assessmentSucceeded(jobs), true); jobs.agent.outputs.output_types = ''; assert.equal(assessmentSucceeded(jobs), false); jobs.safe_outputs.outputs.process_safe_outputs_processed_count = '1'; assert.equal(assessmentSucceeded(jobs), true); }); test('a failed write or safety check cannot be hidden by a no-action result', () => { const jobs = completedJobs(); jobs.agent.outputs.output_types = 'noop'; jobs.safe_outputs.outputs.process_safe_outputs_items_failed = '1'; assert.equal(assessmentSucceeded(jobs), false); jobs.safe_outputs.outputs.process_safe_outputs_items_failed = '0'; for (const name of ['agent', 'detection', 'safe_outputs']) { for (const result of ['failure', 'cancelled', 'skipped']) { jobs[name].result = result; assert.equal(assessmentSucceeded(jobs), false, `${name}: ${result}`); } jobs[name].result = 'success'; } for (const type of ['missing_tool', 'missing_data', 'report_incomplete']) { jobs.agent.outputs.output_types = `noop,${type}`; assert.equal(assessmentSucceeded(jobs), false); } });