From 62039ae65b18eeff123b6781792d0745d11a47a9 Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:54:12 +0800 Subject: [PATCH] fix(import): allow issue-only duplicate closes --- scripts/import-gitcrawl-clusters.mjs | 7 ++- test/import-gitcrawl-clusters.test.mjs | 80 +++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/scripts/import-gitcrawl-clusters.mjs b/scripts/import-gitcrawl-clusters.mjs index 668621dd3..45ac883af 100644 --- a/scripts/import-gitcrawl-clusters.mjs +++ b/scripts/import-gitcrawl-clusters.mjs @@ -274,7 +274,12 @@ for (const clusterId of clusterIds) { `allow_fix_pr: ${allowFixPr ? "true" : "false"}`, `allow_merge: ${allowMerge ? "true" : "false"}`, `allow_post_merge_close: ${allowPostMergeClose ? "true" : "false"}`, - `require_fix_before_close: ${allowFixPr || allowMerge ? "true" : "false"}`, + `require_fix_before_close: ${ + securitySensitiveMembers.length > 0 || + (pullRequestCount > 0 && (allowFixPr || allowMerge)) + ? "true" + : "false" + }`, ] : []), `canonical_hint: ${quoteYaml(canonicalHint(representative, targetRepresentative))}`, diff --git a/test/import-gitcrawl-clusters.test.mjs b/test/import-gitcrawl-clusters.test.mjs index 35e6954a6..b1ae77e8e 100644 --- a/test/import-gitcrawl-clusters.test.mjs +++ b/test/import-gitcrawl-clusters.test.mjs @@ -121,6 +121,68 @@ test("autonomous cluster repair jobs require explicit guarded force-push authori assert.doesNotMatch(job, /blocked_actions:\n(?: - .+\n)* - force_push/); }); +test("issue-only autonomous clusters do not require a fix before duplicate close", { skip: hasSqlite ? false : "sqlite3 missing" }, () => { + const fixture = makeFixture(); + seedPortableGitcrawlDb(fixture.db); + + const result = runWrittenImport( + fixture, + "--mode", + "autonomous", + "--allow-fix-pr", + "false", + "--allow-merge", + "true", + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + const payload = JSON.parse(result.stdout); + const job = fs.readFileSync(path.join(fixture.out, path.basename(payload.generated[0].path)), "utf8"); + + assert.match(job, /require_fix_before_close: false/); + assert.match(job, /require_human_for:\n - security_sensitive\n(?: - .+\n)* - unclear_canonical/); +}); + +test("mixed autonomous clusters require a fix before close when merge is allowed", { skip: hasSqlite ? false : "sqlite3 missing" }, () => { + const fixture = makeFixture(); + seedPortableGitcrawlDb(fixture.db, { secondKind: "pull_request" }); + + const result = runWrittenImport( + fixture, + "--mode", + "autonomous", + "--allow-fix-pr", + "false", + "--allow-merge", + "true", + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + const payload = JSON.parse(result.stdout); + const job = fs.readFileSync(path.join(fixture.out, path.basename(payload.generated[0].path)), "utf8"); + + assert.match(job, /require_fix_before_close: true/); +}); + +test("issue-only autonomous clusters keep fix-first quarantine when any member has a security signal", { skip: hasSqlite ? false : "sqlite3 missing" }, () => { + const fixture = makeFixture(); + seedPortableGitcrawlDb(fixture.db, { firstTitle: "Security issue: first duplicate" }); + + const result = runWrittenImport( + fixture, + "--mode", + "autonomous", + "--allow-fix-pr", + "false", + "--allow-merge", + "true", + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + const payload = JSON.parse(result.stdout); + const job = fs.readFileSync(path.join(fixture.out, path.basename(payload.generated[0].path)), "utf8"); + + assert.match(job, /security_signal_refs:\n - "#101"/); + assert.match(job, /require_fix_before_close: true/); +}); + function runImport(fixture, ...extraArgs) { return spawnSync( process.execPath, @@ -264,7 +326,15 @@ ${rows.map((row) => `| ${row.ref} | ${row.action} | ${row.status} | independent ); } -function seedPortableGitcrawlDb(dbPath, { firstLabels = [], secondLabels = [] } = {}) { +function seedPortableGitcrawlDb( + dbPath, + { + firstLabels = [], + firstTitle = "Bug: first duplicate", + secondLabels = [], + secondKind = "issue", + } = {}, +) { execFileSync("sqlite3", [dbPath], { input: ` create table cluster_groups ( @@ -291,8 +361,8 @@ create table threads ( updated_at text ); insert into threads values - (1, 101, 'issue', 'open', 'Bug: first duplicate', 'body', 'body', '${sqlJson(firstLabels)}', '2026-06-01T00:00:00Z'), - (2, 102, 'issue', 'open', 'Bug: second duplicate', 'body', 'body', '${sqlJson(secondLabels)}', '2026-06-02T00:00:00Z'); + (1, 101, 'issue', 'open', '${sqlText(firstTitle)}', 'body', 'body', '${sqlJson(firstLabels)}', '2026-06-01T00:00:00Z'), + (2, 102, '${secondKind}', 'open', 'Bug: second duplicate', 'body', 'body', '${sqlJson(secondLabels)}', '2026-06-02T00:00:00Z'); insert into cluster_groups values (1, 1, 'active', '2026-06-01T00:00:00Z', null); insert into cluster_memberships values (1, 1, 'active'), @@ -304,3 +374,7 @@ insert into cluster_memberships values function sqlJson(value) { return JSON.stringify(value).replaceAll("'", "''"); } + +function sqlText(value) { + return String(value).replaceAll("'", "''"); +}