From aac066a9c73647180dc4724fc67f658e94a9bf2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Can=20G=C3=BCm=C3=BC=C5=9F?= Date: Sat, 24 Oct 2026 16:54:21 +0300 Subject: [PATCH] Accept mixed-case reconciliation commands (#2205) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-command-parse.test.ts | 45 +++++++++++++++++++++ workflows/shared/squad-command-contract.mjs | 3 +- workflows/squad-workflows.manifest.json | 2 +- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/test/gh-aw-command-parse.test.ts b/test/gh-aw-command-parse.test.ts index a19782436..8abda911d 100644 --- a/test/gh-aw-command-parse.test.ts +++ b/test/gh-aw-command-parse.test.ts @@ -109,6 +109,51 @@ describe('gh-aw: shared /squad command contract (#1824)', () => { }); }); + describe('reconciliation command', () => { + const sha = 'a'.repeat(64); + + it.each([ + `/squad reconcile activation 123 ${sha}`, + `/squad Reconcile Activation 123 ${sha}`, + `/squad rEcOnCiLe aCtIvAtIoN 123 ${sha}`, + ])('accepts command-word casing without changing the SHA: %s', body => { + const result = classifySquadCommand(comment(body), 'issue_comment'); + expect(result).toMatchObject({ + status: 'accepted', + source: 'comment', + mode: 'reconcile activation', + argumentText: body.slice('/squad '.length), + }); + expect(commandRequiresAuthorization(result)).toBe(true); + }); + + it('accepts mixed-case keywords from workflow dispatch', () => { + expect(classifySquadCommand(dispatch(`Reconcile Activation 123 ${sha}`), 'workflow_dispatch')) + .toMatchObject({ status: 'accepted', mode: 'reconcile activation' }); + }); + + it.each([ + `Reconcile Activation 123 ${'A'.repeat(64)}`, + `Reconcile Activation 123 ${'a'.repeat(63)}A`, + `Reconcile Activation 123 ${'a'.repeat(63)}`, + `Reconcile Activation 0 ${sha}`, + `Reconcile Activation 0123 ${sha}`, + `Reconcile Activation -1 ${sha}`, + `Reconcile Activation 123 ${sha} extra`, + ])('rejects invalid IDs, hashes, and trailing arguments: %s', command => { + expect(classifySquadCommand(comment(`/squad ${command}`), 'issue_comment')) + .toMatchObject({ status: 'rejected', reason: 'Unknown command or malformed arguments.' }); + }); + + it('does not route a casing-only comment edit twice', () => { + const previous = `/squad reconcile activation 123 ${sha}`; + const current = `/squad Reconcile Activation 123 ${sha}`; + const payload = { ...comment(current), action: 'edited', changes: { body: { from: previous } } }; + const result = classifySquadCommand(payload, 'issue_comment'); + expect(editedCommandShouldRoute(payload, 'issue_comment', result)).toBe(false); + }); + }); + describe('workflow_dispatch call site', () => { it.each([ ['implement', 'implement', null], diff --git a/workflows/shared/squad-command-contract.mjs b/workflows/shared/squad-command-contract.mjs index aabc9c692..7688b71e6 100644 --- a/workflows/shared/squad-command-contract.mjs +++ b/workflows/shared/squad-command-contract.mjs @@ -177,7 +177,8 @@ function classifyArgument(argumentText) { const normalized = argumentText.replace(/[ \t]+/g, ' ').trim(); const lowered = normalized.toLowerCase(); - if (/^reconcile activation [1-9]\d* [a-f0-9]{64}$/.test(normalized)) { + const reconciliation = normalized.match(/^([A-Za-z]+ [A-Za-z]+) [1-9]\d* [a-f0-9]{64}$/); + if (reconciliation?.[1]?.toLowerCase() === 'reconcile activation') { return accepted('reconcile activation', normalized); } diff --git a/workflows/squad-workflows.manifest.json b/workflows/squad-workflows.manifest.json index 876de092c..bddad02ad 100644 --- a/workflows/squad-workflows.manifest.json +++ b/workflows/squad-workflows.manifest.json @@ -177,7 +177,7 @@ "package_destination": ".github/workflows/shared/squad-command-contract.mjs", "destination": ".github/workflows/shared/squad-command-contract.mjs", "ownership": "manifest", - "sha256": "577e8787ae2e6ccd767e136f97f187166b8bd1d7f599f4c8b86d3bc571daf2de" + "sha256": "9f044d9ab402d893f58eb0477e3d3620540f0b992056ed3a61d090daa50aea4a" }, { "path": "shared/squad-evidence-provider.mjs",