Files
spotifast/.github/scripts/issue-assessment.test.cjs
T
Carmine Paolino 8bd25261c9 Rename the app to Spotifast while preserving installation identities
Use Spotifast across the native app, commands, packages and documentation. Keep existing state, credential stores, protocol identities and package names; provide both command names and compatible updater payloads. Rename the guides with jekyll-redirect-from and prepare spotifast.rocks as the website domain. Keep product descriptions name-free.
2026-09-14 10:43:11 +02:00

313 lines
12 KiB
JavaScript

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: '<!-- gh-aw-failure-issue: true, workflow_id: issue-assessment, branch: main -->',
};
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: '<!-- gh-aw-failure-issue: true, workflow_id: issue-assessment -->' },
{ 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);
}
});