mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
c9e6033048
* Automated issue triage workflow * Bump dependencies * Fix issue-triage workflow: security, reliability, and testability Address six review comments on the issue-triage workflow: 1. Change trigger from issues:opened to issues:labeled so the secret-backed triage flow is only triggered by a maintainer- controlled signal. 2. Include inputs.issue_number in the concurrency group so workflow_dispatch runs for the same issue are properly de-duplicated. 3. Improve team membership error handling to fail closed: verify the team exists before checking membership, and only treat a 404 as 'not a member' (all other errors fail the job). 4. Use optional chaining (issue.user?.login) for the API-fetched issue to handle deleted GitHub accounts without crashing. 5. Extract the inline github-script into a testable module at .github/scripts/check_team_membership.js with 10 tests in .github/tests/test_check_team_membership.js covering all code paths (payload/API author resolution, deleted accounts, team lookup failure, 404 vs non-404 membership errors). 6. Make the spam gate actually stop the job by exiting non-zero instead of just logging, so future steps cannot accidentally run for spam issues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make issue-triage workflow manually triggered only for initial testing Remove the 'issues' event trigger, keeping only 'workflow_dispatch' so the workflow can be tested manually before enabling automatic triggers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
179 lines
6.2 KiB
JavaScript
179 lines
6.2 KiB
JavaScript
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
/**
|
|
* Tests for check_team_membership.js.
|
|
*
|
|
* Run with: node --test .github/tests/test_check_team_membership.js
|
|
*/
|
|
|
|
const { describe, it } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const checkTeamMembership = require('../scripts/check_team_membership.js');
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState = 'active' } = {}) {
|
|
const core = {
|
|
_infoMessages: [],
|
|
_failedMessages: [],
|
|
info(msg) { this._infoMessages.push(msg); },
|
|
setFailed(msg) { this._failedMessages.push(msg); },
|
|
};
|
|
|
|
const context = {
|
|
payload: { issue: payloadIssue },
|
|
repo: { owner: 'test-org', repo: 'test-repo' },
|
|
};
|
|
|
|
const github = {
|
|
rest: {
|
|
issues: {
|
|
get: async () => ({
|
|
data: { user: apiUser ? { login: apiUser } : null },
|
|
}),
|
|
},
|
|
teams: {
|
|
getByName: async () => ({}),
|
|
getMembershipForUserInOrg: async () => ({
|
|
data: { state: teamState },
|
|
}),
|
|
},
|
|
},
|
|
};
|
|
|
|
return { core, context, github };
|
|
}
|
|
|
|
const BASE_OPTS = { teamSlug: 'my-team', issueNumber: '123' };
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Author resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('author resolution', () => {
|
|
it('resolves author from event payload', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: { login: 'payload-user' } },
|
|
});
|
|
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
|
assert.equal(result.author, 'payload-user');
|
|
});
|
|
|
|
it('resolves author via API when payload issue is absent', async () => {
|
|
const { github, context, core } = createMocks({ apiUser: 'api-user' });
|
|
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
|
assert.equal(result.author, 'api-user');
|
|
});
|
|
|
|
it('resolves author via API when payload issue user is null (deleted account)', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: null },
|
|
apiUser: 'fetched-user',
|
|
});
|
|
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
|
assert.equal(result.author, 'fetched-user');
|
|
});
|
|
|
|
it('handles deleted account when API also returns null user', async () => {
|
|
const { github, context, core } = createMocks({ apiUser: null });
|
|
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
|
assert.equal(result.author, null);
|
|
assert.equal(result.isTeamMember, false);
|
|
assert.ok(core._failedMessages.some(m => m.includes('deleted')));
|
|
});
|
|
});
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Team lookup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('team lookup', () => {
|
|
it('fails the job when team lookup errors', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: { login: 'user1' } },
|
|
});
|
|
const error = new Error('Bad credentials');
|
|
github.rest.teams.getByName = async () => { throw error; };
|
|
|
|
await assert.rejects(
|
|
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
|
|
(err) => err === error,
|
|
);
|
|
assert.ok(core._failedMessages.some(m => m.includes('Team lookup failed')));
|
|
});
|
|
});
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Team membership
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('team membership', () => {
|
|
it('returns true for active team member', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: { login: 'member' } },
|
|
teamState: 'active',
|
|
});
|
|
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
|
assert.equal(result.isTeamMember, true);
|
|
});
|
|
|
|
it('returns false for pending team member', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: { login: 'pending-user' } },
|
|
teamState: 'pending',
|
|
});
|
|
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
|
assert.equal(result.isTeamMember, false);
|
|
});
|
|
|
|
it('treats 404 membership response as non-member without failing', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: { login: 'outsider' } },
|
|
});
|
|
const notFoundError = new Error('Not Found');
|
|
notFoundError.status = 404;
|
|
github.rest.teams.getMembershipForUserInOrg = async () => { throw notFoundError; };
|
|
|
|
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
|
assert.equal(result.isTeamMember, false);
|
|
assert.equal(core._failedMessages.length, 0);
|
|
assert.ok(core._infoMessages.some(m => m.includes('not a member')));
|
|
});
|
|
|
|
it('fails the job on non-404 membership errors', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: { login: 'user1' } },
|
|
});
|
|
const serverError = new Error('Internal Server Error');
|
|
serverError.status = 500;
|
|
github.rest.teams.getMembershipForUserInOrg = async () => { throw serverError; };
|
|
|
|
await assert.rejects(
|
|
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
|
|
(err) => err === serverError,
|
|
);
|
|
assert.ok(core._failedMessages.some(m => m.includes('membership lookup failed')));
|
|
});
|
|
|
|
it('fails the job on membership errors without status code', async () => {
|
|
const { github, context, core } = createMocks({
|
|
payloadIssue: { user: { login: 'user1' } },
|
|
});
|
|
const networkError = new Error('ECONNREFUSED');
|
|
github.rest.teams.getMembershipForUserInOrg = async () => { throw networkError; };
|
|
|
|
await assert.rejects(
|
|
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
|
|
(err) => err === networkError,
|
|
);
|
|
assert.ok(core._failedMessages.some(m => m.includes('membership lookup failed')));
|
|
});
|
|
});
|