fix(github): scope PR CI automation switches per linked PR (#2512)

Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Alassane
2026-08-12 13:36:11 -04:00
committed by GitHub
parent 76ccc667ce
commit 7855a4e087
41 changed files with 2803 additions and 790 deletions
@@ -1202,6 +1202,121 @@ func TestHttpTaskCIOptions_DefaultAndPatch(t *testing.T) {
}
}
// TestHttpPatchTaskCIOptions_TargetsOnePRWithoutAffectingSibling covers AC6:
// a PATCH naming one linked PR's repository_id/pr_number sets the switch for
// that PR only; a second linked PR's row is unchanged.
func TestHttpPatchTaskCIOptions_TargetsOnePRWithoutAffectingSibling(t *testing.T) {
router, store := setupControllerStoreTest(t)
ctx := context.Background()
for _, prNumber := range []int{1, 2} {
if err := store.CreateTaskPR(ctx, &TaskPR{
TaskID: "task-1", RepositoryID: "repo-1",
Owner: "acme", Repo: "widget", PRNumber: prNumber,
State: "open", CreatedAt: time.Now().UTC(),
}); err != nil {
t.Fatalf("seed task pr #%d: %v", prNumber, err)
}
}
body := bytes.NewBufferString(`{"repository_id":"repo-1","pr_number":1,"auto_fix_enabled":true}`)
req := httptest.NewRequest(http.MethodPatch, "/api/v1/github/tasks/task-1/ci-options", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
opts1, err := store.GetTaskPRAutomationOptions(ctx, "task-1", "repo-1", 1)
if err != nil {
t.Fatalf("get PR 1 options: %v", err)
}
if !opts1.AutoFixEnabled {
t.Fatalf("PR 1 auto_fix_enabled = false, want true")
}
opts2, err := store.GetTaskPRAutomationOptions(ctx, "task-1", "repo-1", 2)
if err != nil {
t.Fatalf("get PR 2 options: %v", err)
}
if opts2.AutoFixEnabled {
t.Fatalf("PR 2 auto_fix_enabled = true, want unaffected false")
}
}
// TestHttpPatchTaskCIOptions_UnlinkedPRReturns400AndWritesNothing covers AC8.
func TestHttpPatchTaskCIOptions_UnlinkedPRReturns400AndWritesNothing(t *testing.T) {
router, store := setupControllerStoreTest(t)
ctx := context.Background()
if err := store.CreateTaskPR(ctx, &TaskPR{
TaskID: "task-1", RepositoryID: "repo-1",
Owner: "acme", Repo: "widget", PRNumber: 1,
State: "open", CreatedAt: time.Now().UTC(),
}); err != nil {
t.Fatalf("seed task pr: %v", err)
}
body := bytes.NewBufferString(`{"repository_id":"repo-1","pr_number":999,"auto_fix_enabled":true,"auto_fix_prompt_override":"must-not-persist"}`)
req := httptest.NewRequest(http.MethodPatch, "/api/v1/github/tasks/task-1/ci-options", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
opts, err := store.GetTaskPRAutomationOptions(ctx, "task-1", "repo-1", 999)
if err != nil {
t.Fatalf("get unlinked PR options: %v", err)
}
if opts.AutoFixEnabled {
t.Fatal("PATCH for an unlinked PR wrote a row instead of writing nothing")
}
taskOptions, err := store.GetTaskCIOptions(ctx, "task-1")
if err != nil {
t.Fatalf("get task options: %v", err)
}
if taskOptions.AutoFixPromptOverride != nil {
t.Fatalf("invalid PR identity partially updated task options: %+v", taskOptions)
}
}
// TestHttpPatchTaskCIOptions_PartialPRIdentityRejected pins the client error
// for a patch that supplies only one side of the PR identity.
func TestHttpPatchTaskCIOptions_PartialPRIdentityRejected(t *testing.T) {
for name, body := range map[string]string{
"repository only": `{"repository_id":"repo-1","auto_fix_enabled":true}`,
"PR number only": `{"pr_number":1,"auto_fix_enabled":true}`,
} {
t.Run(name, func(t *testing.T) {
router, store := setupControllerStoreTest(t)
ctx := context.Background()
if err := store.CreateTaskPR(ctx, &TaskPR{
TaskID: "task-1", RepositoryID: "repo-1",
Owner: "acme", Repo: "widget", PRNumber: 1,
State: "open", CreatedAt: time.Now().UTC(),
}); err != nil {
t.Fatalf("seed task pr: %v", err)
}
req := httptest.NewRequest(http.MethodPatch, "/api/v1/github/tasks/task-1/ci-options", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
opts, err := store.GetTaskPRAutomationOptions(ctx, "task-1", "repo-1", 1)
if err != nil {
t.Fatalf("get PR options: %v", err)
}
if opts.AutoFixEnabled {
t.Fatal("partial identity patch wrote a row instead of writing nothing")
}
})
}
}
func TestHttpTaskCIOptions_DeniesForeignWorkspaceReadAndUpdate(t *testing.T) {
store := newTestStore(t)
log := newControllerTestLogger()