@@ -106,6 +106,36 @@ tools:
106106 shouldHaveCancel : false ,
107107 description : "Issue workflows use global concurrency with engine ID and slot" ,
108108 },
109+ {
110+ name : "slash_command workflow should have dynamic concurrency with issue/PR number" ,
111+ frontmatter : `---
112+ on:
113+ slash_command:
114+ name: test-bot
115+ tools:
116+ github:
117+ allowed: [list_issues]
118+ ---` ,
119+ filename : "slash-command-workflow.md" ,
120+ expectedConcurrency : `concurrency:
121+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}"` ,
122+ shouldHaveCancel : false ,
123+ description : "slash_command workflows should use dynamic concurrency with issue/PR number without cancellation" ,
124+ },
125+ {
126+ name : "slash_command shorthand workflow should have dynamic concurrency with issue/PR number" ,
127+ frontmatter : `---
128+ on: /test-bot
129+ tools:
130+ github:
131+ allowed: [list_issues]
132+ ---` ,
133+ filename : "slash-command-shorthand-workflow.md" ,
134+ expectedConcurrency : `concurrency:
135+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}"` ,
136+ shouldHaveCancel : false ,
137+ description : "slash_command shorthand workflows should use dynamic concurrency with issue/PR number without cancellation" ,
138+ },
109139 }
110140
111141 for _ , tt := range tests {
@@ -294,6 +324,33 @@ func TestGenerateConcurrencyConfig(t *testing.T) {
294324 group: "custom-group"` ,
295325 description : "Existing concurrency configuration should be preserved" ,
296326 },
327+ {
328+ name : "slash_command input YAML should have dynamic concurrency with issue/PR number" ,
329+ workflowData : & WorkflowData {
330+ On : `on:
331+ slash_command: test-bot
332+ workflow_dispatch:` ,
333+ Concurrency : "" ,
334+ },
335+ isAliasTrigger : false ,
336+ expected : `concurrency:
337+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}"` ,
338+ description : "slash_command (input-level YAML) should use issue/PR number, same as command trigger" ,
339+ },
340+ {
341+ name : "slash_command rendered YAML (issue_comment + workflow_dispatch) should have dynamic concurrency" ,
342+ workflowData : & WorkflowData {
343+ On : `on:
344+ issue_comment:
345+ types: [created]
346+ workflow_dispatch:` ,
347+ Concurrency : "" ,
348+ },
349+ isAliasTrigger : false ,
350+ expected : `concurrency:
351+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"` ,
352+ description : "Rendered slash_command YAML (issue_comment + workflow_dispatch) uses issue number via isIssueWorkflow" ,
353+ },
297354 }
298355
299356 for _ , tt := range tests {
@@ -399,6 +456,24 @@ func TestGenerateJobConcurrencyConfig(t *testing.T) {
399456 group: "gh-aw-copilot-${{ github.workflow }}"` ,
400457 description : "workflow_dispatch combined with schedule should still get default concurrency (not workflow_dispatch-only)" ,
401458 },
459+ {
460+ name : "No default concurrency for slash_command input YAML (pre-rendered)" ,
461+ workflowData : & WorkflowData {
462+ On : "on:\n slash_command: test-bot\n workflow_dispatch:" ,
463+ EngineConfig : & EngineConfig {ID : "copilot" },
464+ },
465+ expected : "" ,
466+ description : "slash_command in input YAML should NOT get default concurrency (isSlashCommandWorkflow detects the synthetic event)" ,
467+ },
468+ {
469+ name : "No default concurrency for slash_command rendered YAML (issue_comment + workflow_dispatch)" ,
470+ workflowData : & WorkflowData {
471+ On : "on:\n issue_comment:\n types: [created]\n workflow_dispatch:" ,
472+ EngineConfig : & EngineConfig {ID : "copilot" },
473+ },
474+ expected : "" ,
475+ description : "Rendered slash_command YAML (issue_comment + workflow_dispatch) should NOT get default concurrency (isIssueWorkflow detects it)" ,
476+ },
402477 }
403478
404479 for _ , tt := range tests {
@@ -740,6 +815,17 @@ func TestBuildConcurrencyGroupKeys(t *testing.T) {
740815 expected : []string {"gh-aw" , "${{ github.workflow }}" },
741816 description : "Other workflows should use just workflow name" ,
742817 },
818+ {
819+ name : "slash_command input YAML should include issue/PR number" ,
820+ workflowData : & WorkflowData {
821+ On : `on:
822+ slash_command: test-bot
823+ workflow_dispatch:` ,
824+ },
825+ isAliasTrigger : false ,
826+ expected : []string {"gh-aw" , "${{ github.workflow }}" , "${{ github.event.issue.number || github.event.pull_request.number }}" },
827+ description : "slash_command (input-level YAML) should include issue/PR number in concurrency group" ,
828+ },
743829 }
744830
745831 for _ , tt := range tests {
@@ -836,3 +922,191 @@ func TestShouldEnableCancelInProgress(t *testing.T) {
836922 })
837923 }
838924}
925+
926+ func TestIsWorkflowDispatchOnly (t * testing.T ) {
927+ tests := []struct {
928+ name string
929+ on string
930+ expected bool
931+ desc string
932+ }{
933+ {
934+ name : "Pure workflow_dispatch should be identified as dispatch-only" ,
935+ on : "on:\n workflow_dispatch:" ,
936+ expected : true ,
937+ desc : "A workflow with only workflow_dispatch is dispatch-only" ,
938+ },
939+ {
940+ name : "workflow_dispatch with inputs should be identified as dispatch-only" ,
941+ on : `on:
942+ workflow_dispatch:
943+ inputs:
944+ environment:
945+ description: "Environment"` ,
946+ expected : true ,
947+ desc : "workflow_dispatch with inputs is still dispatch-only" ,
948+ },
949+ {
950+ name : "No workflow_dispatch should not be identified as dispatch-only" ,
951+ on : `on:
952+ schedule:
953+ - cron: "0 9 * * 1"` ,
954+ expected : false ,
955+ desc : "A workflow without workflow_dispatch is not dispatch-only" ,
956+ },
957+ {
958+ name : "workflow_dispatch combined with schedule should not be dispatch-only" ,
959+ on : `on:
960+ workflow_dispatch:
961+ schedule:
962+ - cron: "0 9 * * 1"` ,
963+ expected : false ,
964+ desc : "schedule is a real trigger so the workflow is not dispatch-only" ,
965+ },
966+ {
967+ name : "workflow_dispatch combined with push should not be dispatch-only" ,
968+ on : `on:
969+ workflow_dispatch:
970+ push:
971+ branches: [main]` ,
972+ expected : false ,
973+ desc : "push makes the workflow not dispatch-only" ,
974+ },
975+ {
976+ name : "workflow_dispatch combined with pull_request should not be dispatch-only" ,
977+ on : `on:
978+ workflow_dispatch:
979+ pull_request:
980+ types: [opened]` ,
981+ expected : false ,
982+ desc : "pull_request makes the workflow not dispatch-only" ,
983+ },
984+ {
985+ name : "workflow_dispatch combined with issues should not be dispatch-only" ,
986+ on : `on:
987+ workflow_dispatch:
988+ issues:
989+ types: [opened]` ,
990+ expected : false ,
991+ desc : "issues makes the workflow not dispatch-only" ,
992+ },
993+ {
994+ name : "slash_command with workflow_dispatch should not be dispatch-only" ,
995+ on : `on:
996+ slash_command: test-bot
997+ workflow_dispatch:` ,
998+ expected : false ,
999+ desc : "slash_command is a synthetic event that expands to issue_comment; its presence means the workflow is not dispatch-only" ,
1000+ },
1001+ {
1002+ name : "slash_command map format with workflow_dispatch should not be dispatch-only" ,
1003+ on : `on:
1004+ slash_command:
1005+ name: test-bot
1006+ workflow_dispatch:` ,
1007+ expected : false ,
1008+ desc : "slash_command in map format is still a synthetic event that makes the workflow not dispatch-only" ,
1009+ },
1010+ }
1011+
1012+ for _ , tt := range tests {
1013+ t .Run (tt .name , func (t * testing.T ) {
1014+ result := isWorkflowDispatchOnly (tt .on )
1015+ if result != tt .expected {
1016+ t .Errorf ("isWorkflowDispatchOnly() for %q = %v, want %v: %s" , tt .name , result , tt .expected , tt .desc )
1017+ }
1018+ })
1019+ }
1020+ }
1021+
1022+ func TestHasSpecialTriggers (t * testing.T ) {
1023+ tests := []struct {
1024+ name string
1025+ on string
1026+ expected bool
1027+ desc string
1028+ }{
1029+ {
1030+ name : "Issue workflow is a special trigger" ,
1031+ on : `on:
1032+ issues:
1033+ types: [opened]` ,
1034+ expected : true ,
1035+ desc : "issues trigger should be detected as special" ,
1036+ },
1037+ {
1038+ name : "PR workflow is a special trigger" ,
1039+ on : `on:
1040+ pull_request:
1041+ types: [opened]` ,
1042+ expected : true ,
1043+ desc : "pull_request trigger should be detected as special" ,
1044+ },
1045+ {
1046+ name : "Push workflow is a special trigger" ,
1047+ on : `on:
1048+ push:
1049+ branches: [main]` ,
1050+ expected : true ,
1051+ desc : "push trigger should be detected as special" ,
1052+ },
1053+ {
1054+ name : "Discussion workflow is a special trigger" ,
1055+ on : `on:
1056+ discussion:
1057+ types: [created]` ,
1058+ expected : true ,
1059+ desc : "discussion trigger should be detected as special" ,
1060+ },
1061+ {
1062+ name : "workflow_dispatch-only is a special trigger" ,
1063+ on : "on:\n workflow_dispatch:" ,
1064+ expected : true ,
1065+ desc : "workflow_dispatch-only is treated as special (explicit user intent)" ,
1066+ },
1067+ {
1068+ name : "slash_command input YAML is a special trigger" ,
1069+ on : `on:
1070+ slash_command: test-bot
1071+ workflow_dispatch:` ,
1072+ expected : true ,
1073+ desc : "slash_command is a synthetic event that should be detected as special" ,
1074+ },
1075+ {
1076+ name : "slash_command map format is a special trigger" ,
1077+ on : `on:
1078+ slash_command:
1079+ name: test-bot
1080+ workflow_dispatch:` ,
1081+ expected : true ,
1082+ desc : "slash_command in map format should also be detected as special" ,
1083+ },
1084+ {
1085+ name : "schedule-only is NOT a special trigger" ,
1086+ on : `on:
1087+ schedule:
1088+ - cron: "0 9 * * 1"` ,
1089+ expected : false ,
1090+ desc : "schedule alone is not a special trigger and should receive default job concurrency" ,
1091+ },
1092+ {
1093+ name : "schedule + workflow_dispatch is NOT a special trigger" ,
1094+ on : `on:
1095+ schedule:
1096+ - cron: "0 9 * * 1"
1097+ workflow_dispatch:` ,
1098+ expected : false ,
1099+ desc : "schedule + workflow_dispatch is not a special trigger and should receive default job concurrency" ,
1100+ },
1101+ }
1102+
1103+ for _ , tt := range tests {
1104+ t .Run (tt .name , func (t * testing.T ) {
1105+ wd := & WorkflowData {On : tt .on }
1106+ result := hasSpecialTriggers (wd )
1107+ if result != tt .expected {
1108+ t .Errorf ("hasSpecialTriggers() for %q = %v, want %v: %s" , tt .name , result , tt .expected , tt .desc )
1109+ }
1110+ })
1111+ }
1112+ }
0 commit comments