mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-03 04:22:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4ebe0af42 | ||
|
|
c8e30cf20d | ||
|
|
526248ebdd | ||
|
|
2f51c98ed0 | ||
|
|
558a31947b | ||
|
|
ebc18e7397 | ||
|
|
607b53b6f8 | ||
|
|
e5168240a4 |
No files matched your search
@@ -196,9 +196,16 @@ jobs:
|
||||
}
|
||||
|
||||
if (event === 'issues') {
|
||||
return set('issue_classify', {
|
||||
issue_number: String(context.payload.issue.number),
|
||||
reason: `issues:${context.payload.action}`,
|
||||
const issue = context.payload.issue;
|
||||
const decision = auto.decideIssuesEventRoute({
|
||||
action: context.payload.action,
|
||||
labels: issue?.labels || [],
|
||||
actorLogin: context.payload.sender?.login,
|
||||
botLogins: process.env.ISSUE_BOT_LOGINS,
|
||||
});
|
||||
return set(decision.kind, {
|
||||
issue_number: String(issue.number),
|
||||
reason: decision.reason,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,6 +271,7 @@ jobs:
|
||||
hasOpenBotPull: Boolean(pull),
|
||||
hasOpenRelatedPull: Boolean(relatedPull),
|
||||
body: comment.body,
|
||||
labels,
|
||||
});
|
||||
if (refined.kind === 'issue_classify') {
|
||||
return set('issue_classify', {
|
||||
@@ -349,6 +357,41 @@ jobs:
|
||||
|
||||
return set('skip', { reason: `unhandled ${event}` });
|
||||
|
||||
ready_for_human_handoff:
|
||||
name: Hand reopened issue to maintainers
|
||||
needs: route
|
||||
if: needs.route.outputs.kind == 'ready_for_human_handoff'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: cursor-source-issue-${{ needs.route.outputs.issue_number || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout helpers
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Apply ready-for-human handoff
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ needs.route.outputs.issue_number }}
|
||||
TRUSTED_COMMENT_AUTHORS: ${{ vars.AUTOMATION_OWN_ACTORS || 'binaricat,netcatty-bot,github-actions[bot]' }}
|
||||
with:
|
||||
github-token: ${{ secrets.TRIAGE_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const auto = require(`${process.env.GITHUB_WORKSPACE}/scripts/cursor-automation.cjs`);
|
||||
await auto.applyReadyForHumanHandoff({
|
||||
github,
|
||||
context,
|
||||
issueNumber: process.env.ISSUE_NUMBER,
|
||||
dedupeMarker: auto.REOPEN_HANDOFF_MARKER,
|
||||
trustedCommentAuthors: process.env.TRUSTED_COMMENT_AUTHORS,
|
||||
});
|
||||
|
||||
cleanup_source_issue:
|
||||
name: Clean source issue state
|
||||
needs: route
|
||||
|
||||
@@ -2253,6 +2253,121 @@ const ISSUE_FOLLOWUP_LABELS = new Set([
|
||||
'triage:unclear',
|
||||
]);
|
||||
|
||||
/** Outcome labels that mean automatic triage already finished once. */
|
||||
const ISSUE_ADMITTED_LABELS = new Set([
|
||||
'triage:admitted',
|
||||
'needs-info',
|
||||
'ready-for-agent',
|
||||
'ready-for-human',
|
||||
'triage:bug-ready',
|
||||
'triage:bug-needs-info',
|
||||
'triage:feature-quick-win',
|
||||
'triage:feature-defer',
|
||||
'triage:already-available',
|
||||
'triage:other',
|
||||
'triage:unclear',
|
||||
'unclear',
|
||||
]);
|
||||
|
||||
/** Auto-closed triage outcomes that a human reopen should hand to maintainers. */
|
||||
const ISSUE_AUTO_CLOSE_HANDOFF_LABELS = new Set([
|
||||
'triage:already-available',
|
||||
'triage:unclear',
|
||||
'unclear',
|
||||
]);
|
||||
|
||||
const REOPEN_HANDOFF_MARKER = '<!-- cursor-reopen-handoff -->';
|
||||
|
||||
function normalizeIssueLabelNames(labels = []) {
|
||||
return (labels || [])
|
||||
.map((label) => (typeof label === 'string' ? label : label?.name))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function isIssueAlreadyAdmitted(labels = []) {
|
||||
return normalizeIssueLabelNames(labels).some((name) =>
|
||||
ISSUE_ADMITTED_LABELS.has(name),
|
||||
);
|
||||
}
|
||||
|
||||
function labelsForReadyForHumanHandoff(existingLabels = []) {
|
||||
const existing = normalizeIssueLabelNames(existingLabels);
|
||||
// Keep triage:already-available / unclear as dispute signals so later author
|
||||
// follow-ups stay on issue_followup instead of re-entering classify.
|
||||
const drop = new Set(['ready-for-agent']);
|
||||
const preserved = existing.includes('triage:admitted')
|
||||
? ['triage:admitted']
|
||||
: [];
|
||||
return [
|
||||
...new Set([
|
||||
...existing.filter((name) => !drop.has(name) && name !== 'ready-for-human'),
|
||||
'triage',
|
||||
'ready-for-human',
|
||||
...preserved,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Route issues opened/reopened without always re-running agent classify.
|
||||
* Reopen of an already-triaged issue must not replay the close/reopen loop.
|
||||
*/
|
||||
function decideIssuesEventRoute({
|
||||
action,
|
||||
labels = [],
|
||||
actorLogin,
|
||||
botLogins = ['netcatty-bot', 'github-actions[bot]'],
|
||||
} = {}) {
|
||||
const normalizedAction = String(action || '').toLowerCase();
|
||||
if (normalizedAction === 'opened') {
|
||||
return { kind: 'issue_classify', reason: 'issues:opened' };
|
||||
}
|
||||
if (normalizedAction !== 'reopened') {
|
||||
return {
|
||||
kind: 'skip',
|
||||
reason: `issues:${normalizedAction || 'unknown'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const names = normalizeIssueLabelNames(labels);
|
||||
const bots = normalizeLoginList(botLogins, [
|
||||
'netcatty-bot',
|
||||
'github-actions[bot]',
|
||||
]);
|
||||
const actor = String(actorLogin || '').trim().toLowerCase();
|
||||
if (actor && bots.has(actor)) {
|
||||
return {
|
||||
kind: 'skip',
|
||||
reason: 'bot reopen of managed issue',
|
||||
};
|
||||
}
|
||||
if (!isIssueAlreadyAdmitted(names)) {
|
||||
return { kind: 'issue_classify', reason: 'issues:reopened' };
|
||||
}
|
||||
if (names.some((name) => ISSUE_AUTO_CLOSE_HANDOFF_LABELS.has(name))) {
|
||||
return {
|
||||
kind: 'ready_for_human_handoff',
|
||||
reason: 'human reopen of auto-closed triage',
|
||||
};
|
||||
}
|
||||
// Merged-fix / bug-ready / feature reopens must re-enter classify so a
|
||||
// failed fix can spawn another implementation. Bot reopen is already skipped
|
||||
// above, which covers follow-up reopen side effects.
|
||||
return {
|
||||
kind: 'issue_classify',
|
||||
reason: 'issues:reopened admitted non-auto-close',
|
||||
};
|
||||
}
|
||||
|
||||
function buildReopenHandoffReply(issue = {}) {
|
||||
const chinese = /[\u3400-\u9fff]/u.test(
|
||||
`${issue.title || ''}\n${issue.body || ''}`,
|
||||
);
|
||||
return chinese
|
||||
? '这条 Issue 已重新打开,并交给维护者继续处理。自动分流不会再次把它标成“已有能力/可关闭”。'
|
||||
: 'This issue was reopened and handed to a maintainer. Automatic triage will not close it again as already available.';
|
||||
}
|
||||
|
||||
function normalizeLoginList(value, fallback = []) {
|
||||
const values = Array.isArray(value) ? value : String(value || '').split(',');
|
||||
const normalized = values
|
||||
@@ -2331,8 +2446,17 @@ function refineIssueCommentRoute(decision, {
|
||||
hasOpenBotPull = false,
|
||||
hasOpenRelatedPull = false,
|
||||
body = '',
|
||||
labels = [],
|
||||
} = {}) {
|
||||
if (decision?.kind !== 'issue_followup' || hasOpenBotPull) return decision;
|
||||
const names = normalizeIssueLabelNames(labels);
|
||||
// Disputed auto-close outcomes (already_available / unclear) must stay on
|
||||
// follow-up after reopen handoff. Do not gate on ready-for-human alone —
|
||||
// that label also covers feature_defer / other / implement failures that
|
||||
// should still be allowed to re-enter classify.
|
||||
if (names.some((name) => ISSUE_AUTO_CLOSE_HANDOFF_LABELS.has(name))) {
|
||||
return decision;
|
||||
}
|
||||
const simpleKind = classifySimpleIssueFollowup([{ body }]);
|
||||
if (simpleKind) return decision;
|
||||
return {
|
||||
@@ -3010,6 +3134,8 @@ async function markNeedsHuman({
|
||||
message,
|
||||
dedupeMarker = '',
|
||||
trustedCommentAuthors = 'binaricat,netcatty-bot,github-actions[bot]',
|
||||
labels,
|
||||
ensureOpen = false,
|
||||
}) {
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
@@ -3019,19 +3145,19 @@ async function markNeedsHuman({
|
||||
issue_number: Number(issueNumber),
|
||||
});
|
||||
const existing = issue.labels.map((l) => (typeof l === 'string' ? l : l.name));
|
||||
const next = [
|
||||
...new Set([
|
||||
...existing.filter((l) => l !== 'ready-for-agent'),
|
||||
'triage',
|
||||
'ready-for-human',
|
||||
]),
|
||||
];
|
||||
await github.rest.issues.update({
|
||||
const next = Array.isArray(labels)
|
||||
? [...new Set(labels.filter(Boolean))]
|
||||
: labelsForReadyForHumanHandoff(existing);
|
||||
const update = {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
labels: next,
|
||||
});
|
||||
};
|
||||
if (ensureOpen && String(issue.state || '').toLowerCase() === 'closed') {
|
||||
update.state = 'open';
|
||||
}
|
||||
await github.rest.issues.update(update);
|
||||
const marker = String(dedupeMarker || '').trim();
|
||||
if (marker) {
|
||||
const trusted = normalizeLoginList(trustedCommentAuthors, [
|
||||
@@ -3065,6 +3191,53 @@ async function markNeedsHuman({
|
||||
return { issue, labels: next, commented: true };
|
||||
}
|
||||
|
||||
async function applyReadyForHumanHandoff({
|
||||
github,
|
||||
context,
|
||||
issueNumber,
|
||||
message,
|
||||
dedupeMarker = REOPEN_HANDOFF_MARKER,
|
||||
trustedCommentAuthors = 'binaricat,netcatty-bot,github-actions[bot]',
|
||||
} = {}) {
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
...context.repo,
|
||||
issue_number: Number(issueNumber),
|
||||
});
|
||||
const names = normalizeIssueLabelNames(issue.labels);
|
||||
// Reopen already left the issue open. If a maintainer re-closed it while this
|
||||
// handoff was queued, do not force it open again.
|
||||
if (String(issue.state || '').toLowerCase() === 'closed') {
|
||||
return {
|
||||
issue,
|
||||
labels: names,
|
||||
commented: false,
|
||||
skipped: true,
|
||||
reason: 'issue already closed',
|
||||
};
|
||||
}
|
||||
// Revalidate at apply time: a queued handoff must not undo a maintainer who
|
||||
// already cleared the auto-close outcome and moved the issue elsewhere.
|
||||
if (!names.some((name) => ISSUE_AUTO_CLOSE_HANDOFF_LABELS.has(name))) {
|
||||
return {
|
||||
issue,
|
||||
labels: names,
|
||||
commented: false,
|
||||
skipped: true,
|
||||
reason: 'auto-close handoff labels no longer present',
|
||||
};
|
||||
}
|
||||
return markNeedsHuman({
|
||||
github,
|
||||
context,
|
||||
issueNumber,
|
||||
message: message || buildReopenHandoffReply(issue),
|
||||
dedupeMarker,
|
||||
trustedCommentAuthors,
|
||||
labels: labelsForReadyForHumanHandoff(issue.labels),
|
||||
ensureOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
function isBotPrForIssue(pull, issueNumber) {
|
||||
if (!pull) return false;
|
||||
const n = String(issueNumber);
|
||||
@@ -3709,9 +3882,18 @@ module.exports = {
|
||||
CODEX_REQUEST_RETRY_MS,
|
||||
decideCodexLoopAction,
|
||||
shouldReTriageIssueComment,
|
||||
ISSUE_ADMITTED_LABELS,
|
||||
ISSUE_AUTO_CLOSE_HANDOFF_LABELS,
|
||||
REOPEN_HANDOFF_MARKER,
|
||||
normalizeIssueLabelNames,
|
||||
isIssueAlreadyAdmitted,
|
||||
labelsForReadyForHumanHandoff,
|
||||
decideIssuesEventRoute,
|
||||
buildReopenHandoffReply,
|
||||
mentionsIssueBot,
|
||||
decideIssueCommentRoute,
|
||||
refineIssueCommentRoute,
|
||||
applyReadyForHumanHandoff,
|
||||
formatCodexFindingsMarkdown,
|
||||
listProtectedPathHits,
|
||||
formatProtectedPathDetails,
|
||||
|
||||
@@ -627,6 +627,119 @@ test('actionable author follow-ups without an open bot PR are reclassified', ()
|
||||
}),
|
||||
decision,
|
||||
);
|
||||
assert.equal(
|
||||
auto.refineIssueCommentRoute(decision, {
|
||||
hasOpenBotPull: false,
|
||||
labels: ['triage:already-available', 'triage:admitted'],
|
||||
body: '我从 main build 了,还是一样,本地网络权限没有弹窗。',
|
||||
}),
|
||||
decision,
|
||||
);
|
||||
assert.equal(
|
||||
auto.refineIssueCommentRoute(decision, {
|
||||
hasOpenBotPull: false,
|
||||
labels: ['triage:unclear', 'unclear', 'ready-for-human'],
|
||||
body: '补充:复现步骤是打开 Vault 再连局域网主机。',
|
||||
}),
|
||||
decision,
|
||||
);
|
||||
// ready-for-human alone must NOT block reclassify (feature_defer / other /
|
||||
// implement-failure handoffs still need actionable follow-ups to classify).
|
||||
assert.deepEqual(
|
||||
auto.refineIssueCommentRoute(decision, {
|
||||
hasOpenBotPull: false,
|
||||
labels: ['triage', 'triage:feature-defer', 'ready-for-human'],
|
||||
body: '默认开启 X11 就可以,内置服务暂时不需要。',
|
||||
}),
|
||||
{
|
||||
kind: 'issue_classify',
|
||||
reason: 'actionable author follow-up without open automation PR',
|
||||
},
|
||||
);
|
||||
// After reopen handoff, triage:already-available is preserved with
|
||||
// ready-for-human so disputes cannot re-close via classify.
|
||||
assert.equal(
|
||||
auto.refineIssueCommentRoute(decision, {
|
||||
hasOpenBotPull: false,
|
||||
labels: [
|
||||
'triage',
|
||||
'triage:admitted',
|
||||
'triage:already-available',
|
||||
'ready-for-human',
|
||||
],
|
||||
body: '我从 main build 了,还是一样,本地网络权限没有弹窗。',
|
||||
}),
|
||||
decision,
|
||||
);
|
||||
});
|
||||
|
||||
test('decideIssuesEventRoute skips bot reopen and hands auto-closed reopen to humans', () => {
|
||||
assert.deepEqual(
|
||||
auto.decideIssuesEventRoute({ action: 'opened', labels: [] }),
|
||||
{ kind: 'issue_classify', reason: 'issues:opened' },
|
||||
);
|
||||
assert.deepEqual(
|
||||
auto.decideIssuesEventRoute({
|
||||
action: 'reopened',
|
||||
labels: ['bug', 'triage'],
|
||||
actorLogin: 'alice',
|
||||
}),
|
||||
{ kind: 'issue_classify', reason: 'issues:reopened' },
|
||||
);
|
||||
assert.deepEqual(
|
||||
auto.decideIssuesEventRoute({
|
||||
action: 'reopened',
|
||||
labels: ['triage:admitted', 'triage:already-available'],
|
||||
actorLogin: 'netcatty-bot',
|
||||
}),
|
||||
{ kind: 'skip', reason: 'bot reopen of managed issue' },
|
||||
);
|
||||
assert.deepEqual(
|
||||
auto.decideIssuesEventRoute({
|
||||
action: 'reopened',
|
||||
labels: ['triage:admitted', 'triage:already-available'],
|
||||
actorLogin: 'binaricat',
|
||||
}),
|
||||
{
|
||||
kind: 'ready_for_human_handoff',
|
||||
reason: 'human reopen of auto-closed triage',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
auto.decideIssuesEventRoute({
|
||||
action: 'reopened',
|
||||
labels: ['triage:admitted', 'ready-for-human', 'triage'],
|
||||
actorLogin: 'binaricat',
|
||||
}),
|
||||
{
|
||||
kind: 'issue_classify',
|
||||
reason: 'issues:reopened admitted non-auto-close',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
auto.decideIssuesEventRoute({
|
||||
action: 'reopened',
|
||||
labels: ['triage:admitted', 'triage:bug-ready', 'bug'],
|
||||
actorLogin: 'alice',
|
||||
}),
|
||||
{
|
||||
kind: 'issue_classify',
|
||||
reason: 'issues:reopened admitted non-auto-close',
|
||||
},
|
||||
);
|
||||
const handoff = auto.labelsForReadyForHumanHandoff([
|
||||
'bug',
|
||||
'triage',
|
||||
'triage:admitted',
|
||||
'triage:already-available',
|
||||
'ready-for-agent',
|
||||
]);
|
||||
assert.ok(handoff.includes('ready-for-human'));
|
||||
assert.ok(handoff.includes('triage:admitted'));
|
||||
assert.ok(handoff.includes('triage:already-available'));
|
||||
assert.ok(!handoff.includes('ready-for-agent'));
|
||||
assert.equal(auto.isIssueAlreadyAdmitted(['triage:admitted']), true);
|
||||
assert.equal(auto.isIssueAlreadyAdmitted(['bug', 'triage']), false);
|
||||
});
|
||||
|
||||
test('decideIssueCommentRoute accepts maintainer @bot and ignores untrusted bystanders', () => {
|
||||
@@ -947,6 +1060,7 @@ test('protected path reports replace stale data and ignore ordinary source files
|
||||
|
||||
test('markNeedsHuman ignores forged dedupe markers from untrusted commenters', async () => {
|
||||
let created = 0;
|
||||
let lastUpdate = null;
|
||||
let comments = [{
|
||||
user: { login: 'mallory' },
|
||||
body: '<!-- cursor-implement-failure:base=abc;kind=no_changes -->',
|
||||
@@ -954,8 +1068,21 @@ test('markNeedsHuman ignores forged dedupe markers from untrusted commenters', a
|
||||
const github = {
|
||||
rest: {
|
||||
issues: {
|
||||
get: async () => ({ data: { number: 42, labels: [] } }),
|
||||
update: async () => ({ data: {} }),
|
||||
get: async () => ({
|
||||
data: {
|
||||
number: 42,
|
||||
state: 'open',
|
||||
labels: [
|
||||
{ name: 'triage:already-available' },
|
||||
{ name: 'triage:admitted' },
|
||||
{ name: 'ready-for-agent' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
update: async (args) => {
|
||||
lastUpdate = args;
|
||||
return { data: {} };
|
||||
},
|
||||
listComments: Symbol('listComments'),
|
||||
createComment: async () => { created += 1; return { data: {} }; },
|
||||
},
|
||||
@@ -972,6 +1099,10 @@ test('markNeedsHuman ignores forged dedupe markers from untrusted commenters', a
|
||||
const first = await auto.markNeedsHuman(args);
|
||||
assert.equal(first.commented, true);
|
||||
assert.equal(created, 1);
|
||||
assert.ok(lastUpdate.labels.includes('ready-for-human'));
|
||||
assert.ok(lastUpdate.labels.includes('triage:admitted'));
|
||||
assert.ok(lastUpdate.labels.includes('triage:already-available'));
|
||||
assert.ok(!lastUpdate.labels.includes('ready-for-agent'));
|
||||
|
||||
comments = [{
|
||||
user: { login: 'netcatty-bot' },
|
||||
@@ -982,6 +1113,128 @@ test('markNeedsHuman ignores forged dedupe markers from untrusted commenters', a
|
||||
assert.equal(created, 1);
|
||||
});
|
||||
|
||||
test('applyReadyForHumanHandoff hands open auto-closed issues to humans', async () => {
|
||||
let update = null;
|
||||
let commentBody = '';
|
||||
const github = {
|
||||
rest: {
|
||||
issues: {
|
||||
get: async () => ({
|
||||
data: {
|
||||
number: 2673,
|
||||
state: 'open',
|
||||
title: '[Bug] 不能连接本地网络',
|
||||
body: '本地网络权限',
|
||||
labels: [
|
||||
{ name: 'triage' },
|
||||
{ name: 'triage:admitted' },
|
||||
{ name: 'triage:already-available' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
update: async (args) => {
|
||||
update = args;
|
||||
return { data: {} };
|
||||
},
|
||||
listComments: Symbol('listComments'),
|
||||
createComment: async (args) => {
|
||||
commentBody = args.body;
|
||||
return { data: {} };
|
||||
},
|
||||
},
|
||||
},
|
||||
paginate: async () => [],
|
||||
};
|
||||
const result = await auto.applyReadyForHumanHandoff({
|
||||
github,
|
||||
context: { repo: { owner: 'binaricat', repo: 'Netcatty' } },
|
||||
issueNumber: 2673,
|
||||
});
|
||||
assert.equal(result.commented, true);
|
||||
assert.equal(update.state, undefined);
|
||||
assert.ok(update.labels.includes('ready-for-human'));
|
||||
assert.ok(update.labels.includes('triage:already-available'));
|
||||
assert.match(commentBody, /cursor-reopen-handoff/);
|
||||
assert.match(commentBody, /重新打开/);
|
||||
});
|
||||
|
||||
test('applyReadyForHumanHandoff skips when auto-close labels were cleared', async () => {
|
||||
let updated = false;
|
||||
const github = {
|
||||
rest: {
|
||||
issues: {
|
||||
get: async () => ({
|
||||
data: {
|
||||
number: 2673,
|
||||
state: 'open',
|
||||
title: '[Bug] 不能连接本地网络',
|
||||
body: '本地网络权限',
|
||||
labels: [
|
||||
{ name: 'triage' },
|
||||
{ name: 'triage:admitted' },
|
||||
{ name: 'ready-for-agent' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
update: async () => {
|
||||
updated = true;
|
||||
return { data: {} };
|
||||
},
|
||||
listComments: Symbol('listComments'),
|
||||
createComment: async () => ({ data: {} }),
|
||||
},
|
||||
},
|
||||
paginate: async () => [],
|
||||
};
|
||||
const result = await auto.applyReadyForHumanHandoff({
|
||||
github,
|
||||
context: { repo: { owner: 'binaricat', repo: 'Netcatty' } },
|
||||
issueNumber: 2673,
|
||||
});
|
||||
assert.equal(result.skipped, true);
|
||||
assert.equal(result.commented, false);
|
||||
assert.equal(updated, false);
|
||||
});
|
||||
|
||||
test('applyReadyForHumanHandoff skips when maintainer already re-closed', async () => {
|
||||
let updated = false;
|
||||
const github = {
|
||||
rest: {
|
||||
issues: {
|
||||
get: async () => ({
|
||||
data: {
|
||||
number: 2673,
|
||||
state: 'closed',
|
||||
title: '[Bug] 不能连接本地网络',
|
||||
body: '本地网络权限',
|
||||
labels: [
|
||||
{ name: 'triage' },
|
||||
{ name: 'triage:admitted' },
|
||||
{ name: 'triage:already-available' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
update: async () => {
|
||||
updated = true;
|
||||
return { data: {} };
|
||||
},
|
||||
listComments: Symbol('listComments'),
|
||||
createComment: async () => ({ data: {} }),
|
||||
},
|
||||
},
|
||||
paginate: async () => [],
|
||||
};
|
||||
const result = await auto.applyReadyForHumanHandoff({
|
||||
github,
|
||||
context: { repo: { owner: 'binaricat', repo: 'Netcatty' } },
|
||||
issueNumber: 2673,
|
||||
});
|
||||
assert.equal(result.skipped, true);
|
||||
assert.equal(result.reason, 'issue already closed');
|
||||
assert.equal(result.commented, false);
|
||||
assert.equal(updated, false);
|
||||
});
|
||||
|
||||
test('workflow cleans source labels after eligible PR close and dedupes clean notices', () => {
|
||||
const workflow = fs.readFileSync(
|
||||
path.join(__dirname, '..', '.github', 'workflows', 'cursor-automation.yml'),
|
||||
@@ -1000,6 +1253,14 @@ test('workflow cleans source labels after eligible PR close and dedupes clean no
|
||||
assert.match(workflow, /does not currently close issue/);
|
||||
assert.match(workflow, /findOpenPullForIssue/);
|
||||
assert.match(workflow, /refineIssueCommentRoute/);
|
||||
assert.match(
|
||||
workflow,
|
||||
/refineIssueCommentRoute\(decision, \{\n\s+hasOpenBotPull: Boolean\(pull\),\n\s+hasOpenRelatedPull: Boolean\(relatedPull\),\n\s+body: comment\.body,\n\s+labels,/,
|
||||
);
|
||||
assert.match(workflow, /decideIssuesEventRoute/);
|
||||
assert.match(workflow, /kind == 'ready_for_human_handoff'/);
|
||||
assert.match(workflow, /applyReadyForHumanHandoff/);
|
||||
assert.match(workflow, /REOPEN_HANDOFF_MARKER/);
|
||||
assert.match(workflow, /issueNumber: issue\.number,\n\s+includeRelated: true/);
|
||||
assert.match(workflow, /reconcile_closed_handoffs:/);
|
||||
assert.match(workflow, /shouldRetryIssueHandoff/);
|
||||
@@ -1018,6 +1279,8 @@ test('workflow cleans source labels after eligible PR close and dedupes clean no
|
||||
route,
|
||||
/sameRepo\s*&&\s*\n\s*context\.payload\.action === 'closed'/,
|
||||
);
|
||||
assert.match(route, /decideIssuesEventRoute/);
|
||||
assert.match(route, /ready_for_human_handoff:/);
|
||||
assert.match(workflow, /Follow-up changed before the simple reply; dispatched a fresh review/);
|
||||
assert.match(workflow, /is merged; skipped stale follow-up handoff state changes/);
|
||||
assert.match(workflow, /trusted_comment_bodies/);
|
||||
|
||||
Reference in new issue
Block a user