-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcommit_request_attach_test.go
More file actions
616 lines (532 loc) · 24.9 KB
/
Copy pathcommit_request_attach_test.go
File metadata and controls
616 lines (532 loc) · 24.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
// SPDX-License-Identifier: Apache-2.0
package git
import (
"context"
"testing"
"time"
gogit "github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/config"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
configv1alpha3 "github.com/ConfigButler/gitops-reverser/api/v1alpha3"
)
// crName is the single CommitRequest name these tests register; each test uses one.
// crTarget is the GitTarget the worker serves (see createPlainGitTarget).
const (
crName = "save"
crTarget = "team-a"
)
// serviceAttach drives one attach + servicing pass on the loop, the way run()
// does after dequeuing an attach work item.
func serviceAttach(loop *branchWorkerEventLoop, req *AttachCommitRequest) {
loop.handleAttachCommitRequest(req)
loop.serviceCommitRequests()
}
func attachReq(author string, closeDelaySeconds int32) *AttachCommitRequest {
return &AttachCommitRequest{
Namespace: "default",
Name: crName,
UID: "uid-" + crName,
Author: author,
GitTargetName: crTarget,
GitTargetNamespace: "default",
CloseDelaySeconds: closeDelaySeconds,
}
}
// forceDue backdates the registered request's finalize deadline so the next
// serviceCommitRequests treats its grace as elapsed — deterministic without sleeping.
func forceDue(loop *branchWorkerEventLoop) {
id := commitRequestID{Namespace: "default", Name: crName, UID: "uid-" + crName}
loop.pendingCRs[id].finalizeAt = time.Now().Add(-time.Millisecond)
}
func outcome(t *testing.T, w *BranchWorker) (FinalizeResult, bool) {
t.Helper()
return w.LookupCommitRequestOutcome("default", crName, "uid-"+crName)
}
// TestEnqueueAttach_Success verifies an attach lands on the worker's event queue.
func TestEnqueueAttach_Success(t *testing.T) {
w := &BranchWorker{Log: logr.Discard(), Branch: "main", eventQueue: make(chan WorkItem, 1)}
req := attachReq("alice", 0)
w.EnqueueAttach(req)
item := <-w.eventQueue
require.NotNil(t, item.Attach)
assert.Same(t, req, item.Attach)
assert.Nil(t, item.Request)
}
// TestEnqueueAttach_QueueFullIsDropped verifies a saturated queue drops the attach
// (the controller re-sends on its next poll) rather than blocking.
func TestEnqueueAttach_QueueFullIsDropped(t *testing.T) {
w := &BranchWorker{Log: logr.Discard(), Branch: "main", eventQueue: make(chan WorkItem, 1)}
w.eventQueue <- WorkItem{} // saturate
w.EnqueueAttach(attachReq("alice", 0))
assert.Zero(t, w.inflightItems.Load(), "a dropped attach must not leak an inflight count")
}
// TestEnqueueAttach_Nil is a no-op and must not panic.
func TestEnqueueAttach_Nil(t *testing.T) {
w := &BranchWorker{Log: logr.Discard(), eventQueue: make(chan WorkItem, 1)}
w.EnqueueAttach(nil)
assert.Empty(t, w.eventQueue)
}
// TestAttach_NoOpenWindow verifies an attach (closeDelaySeconds 0) with nothing pending
// resolves NoOpenWindow — the author pressed save with no edits, not an error.
func TestAttach_NoOpenWindow(t *testing.T) {
worker, _, _ := setupCommitPushSplitWorker(t)
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
serviceAttach(loop, attachReq("alice", 0))
res, ok := outcome(t, worker)
require.True(t, ok, "a 0-grace attach with no window must resolve immediately")
require.NoError(t, res.Err)
assert.Equal(t, FinalizeNoOpenWindow, res.Outcome)
assert.Equal(t, "main", res.Branch)
}
// TestAttach_CommitsOpenWindow verifies a 0-grace attach onto an already-open
// same-author window finalizes it with the request's message and reports the SHA
// (UC1, the "Save" button).
func TestAttach_CommitsOpenWindow(t *testing.T) {
worker, _, remoteURL := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
// lastPushAt left zero so the finalize's push fires immediately; the request is
// resolved on that push (§6.5).
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{
configMapTargetEvent("explicit-a", "alice", "team-a"),
configMapTargetEvent("explicit-b", "alice", "team-a"),
},
CommitMode: CommitModePerEvent,
}})
require.NotNil(t, loop.openWindow, "events should accumulate in an open window")
const message = "save: increase checkout API memory"
req := attachReq("alice", 0)
req.Message = message
serviceAttach(loop, req)
res, ok := outcome(t, worker)
require.True(t, ok)
require.NoError(t, res.Err)
assert.Equal(t, FinalizeCommitted, res.Outcome)
require.NotEmpty(t, res.SHA)
assert.Nil(t, loop.openWindow, "the open window must be finalized")
assert.Empty(t, loop.pendingWrites, "the pushed write is cleared on success")
// The reported SHA must match local HEAD (Committed means on the remote) and
// carry the attached message verbatim.
repo, err := gogit.PlainOpen(worker.repoPathForRemote(remoteURL))
require.NoError(t, err)
ref, err := repo.Reference(plumbing.NewBranchReferenceName("main"), true)
require.NoError(t, err)
assert.Equal(t, ref.Hash().String(), res.SHA)
commit, err := repo.CommitObject(ref.Hash())
require.NoError(t, err)
assert.Equal(t, message, commit.Message, "the attached message must be used verbatim")
assert.Equal(t, "alice", commit.Author.Name)
}
// TestAttach_EmptyMessageUsesGeneratedMessage verifies an attach with no message
// falls back to the generated grouped-commit message.
func TestAttach_EmptyMessageUsesGeneratedMessage(t *testing.T) {
worker, _, remoteURL := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("solo", "bob", "team-a")},
CommitMode: CommitModePerEvent,
}})
require.NotNil(t, loop.openWindow)
serviceAttach(loop, attachReq("bob", 0)) // no Message
res, ok := outcome(t, worker)
require.True(t, ok)
require.NoError(t, res.Err)
assert.Equal(t, FinalizeCommitted, res.Outcome)
repo, err := gogit.PlainOpen(worker.repoPathForRemote(remoteURL))
require.NoError(t, err)
ref, err := repo.Reference(plumbing.NewBranchReferenceName("main"), true)
require.NoError(t, err)
commit, err := repo.CommitObject(ref.Hash())
require.NoError(t, err)
assert.NotEmpty(t, commit.Message)
assert.Equal(t, "bob", commit.Author.Name)
}
// TestAttach_CollectGraceJoinsLaterWindow pins UC2: an attach that arrives before
// the work (no window yet) parks for the grace, attaches when the same-author
// window opens, and finalizes the collected window at the deadline.
func TestAttach_CollectGraceJoinsLaterWindow(t *testing.T) {
worker, _, _ := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
// Attach first, with a non-zero grace, before any window exists.
req := attachReq("alice", 60)
req.Message = "bundle save"
serviceAttach(loop, req)
_, resolved := outcome(t, worker)
require.False(t, resolved, "the request must park, not resolve, while no window exists")
// The work arrives during the grace and opens a window; it must attach.
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("late", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
loop.serviceCommitRequests()
require.NotNil(t, loop.openWindow)
require.NotNil(t, loop.openWindow.pendingCR, "the opened window must carry the attached request")
assert.Equal(t, "bundle save", loop.openWindow.pendingMessage)
// Grace elapses → the collected window is finalized as one commit.
forceDue(loop)
loop.serviceCommitRequests()
res, ok := outcome(t, worker)
require.True(t, ok)
require.NoError(t, res.Err)
assert.Equal(t, FinalizeCommitted, res.Outcome)
assert.Nil(t, loop.openWindow)
}
// TestAttach_ForeignWindowIsNotStolen verifies an attach for a different author
// parks (never finalizes another author's window) and resolves NoOpenWindow once
// its grace elapses, leaving the foreign window open.
func TestAttach_ForeignWindowIsNotStolen(t *testing.T) {
worker, _, _ := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
loop := newBranchWorkerEventLoop(worker, time.Hour)
loop.lastPushAt = time.Now()
defer loop.stopTimers()
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("cm", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
require.NotNil(t, loop.openWindow)
serviceAttach(loop, attachReq("bob", 60)) // bob, not alice
require.Nil(t, loop.openWindow.pendingCR, "bob's attach must not claim alice's window")
forceDue(loop)
loop.serviceCommitRequests()
res, ok := outcome(t, worker)
require.True(t, ok)
require.NoError(t, res.Err)
assert.Equal(t, FinalizeNoOpenWindow, res.Outcome, "another author's save must not finalize alice's window")
require.NotNil(t, loop.openWindow, "alice's window must be left open")
assert.Equal(t, "alice", loop.openWindow.Author)
}
// TestAttach_IdempotentReSendKeepsFirstDeadline verifies a re-sent attach (same
// identity) does not reset the finalize deadline or duplicate the registration.
func TestAttach_IdempotentReSendKeepsFirstDeadline(t *testing.T) {
worker, _, _ := setupCommitPushSplitWorker(t)
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
serviceAttach(loop, attachReq("alice", 60))
id := commitRequestID{Namespace: "default", Name: "save", UID: "uid-save"}
require.Contains(t, loop.pendingCRs, id)
firstDeadline := loop.pendingCRs[id].finalizeAt
serviceAttach(loop, attachReq("alice", 300)) // larger grace, re-send
require.Len(t, loop.pendingCRs, 1, "a re-send must not duplicate the registration")
assert.Equal(t, firstDeadline, loop.pendingCRs[id].finalizeAt, "the first deadline must be kept")
}
// TestAttach_FinalizeFailureResolvesFailed verifies that when the attached
// window's commit fails (unreachable remote) the request resolves with an error.
func TestAttach_FinalizeFailureResolvesFailed(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, configv1alpha3.AddToScheme(scheme))
k8sClient := fake.NewClientBuilder().WithScheme(scheme).Build()
provider := &configv1alpha3.GitProvider{
Spec: configv1alpha3.GitProviderSpec{URL: "file:///nonexistent/gitops-reverser-repo.git"},
}
provider.Name = "test-repo"
provider.Namespace = "default"
require.NoError(t, k8sClient.Create(ctx, provider))
worker := NewBranchWorker(k8sClient, logr.Discard(), "test-repo", "default", "main", nil, 0)
worker.ctx = ctx
createPlainGitTarget(t, worker, "team-a", "team-a")
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("cm", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
require.NotNil(t, loop.openWindow)
serviceAttach(loop, attachReq("alice", 0))
res, ok := outcome(t, worker)
require.True(t, ok)
require.Error(t, res.Err, "an unreachable remote must resolve the request with an error")
assert.Nil(t, loop.openWindow, "a failed finalize still drops the broken window")
}
// TestFinalizeOpenWindow_ReturnsCommittedFlag verifies the boolean contract of
// finalizeOpenWindow: false when there is nothing to finalize, true otherwise.
func TestFinalizeOpenWindow_ReturnsCommittedFlag(t *testing.T) {
worker, _, _ := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
loop := newBranchWorkerEventLoop(worker, time.Hour)
loop.lastPushAt = time.Now()
defer loop.stopTimers()
assert.False(t, loop.finalizeOpenWindow(), "no open window → false")
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("cm", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
require.NotNil(t, loop.openWindow)
assert.True(t, loop.finalizeOpenWindow(), "open window finalized → true")
assert.Nil(t, loop.openWindow)
}
// TestAttach_NoDiffResolvesAlreadyPresentPromptly is the §8.4 pin: a finalize whose
// events re-assert already-present state produces no diff, so the request resolves
// AlreadyPresent at finalize — promptly, never blocking on a push that never comes.
func TestAttach_NoDiffResolvesAlreadyPresentPromptly(t *testing.T) {
worker, _, _ := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
// First, commit the ConfigMap (no CommitRequest) and push it, so it is already
// present in Git.
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("present", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
require.True(t, loop.finalizeOpenWindow())
loop.pushPending()
require.Empty(t, loop.pendingWrites)
// Defer any further push so we can prove the resolution does NOT wait on one.
loop.lastPushAt = time.Now()
// A second window re-asserts the SAME object: no diff. Attach a CommitRequest and
// finalize it (closeDelaySeconds 0).
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("present", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
require.NotNil(t, loop.openWindow)
serviceAttach(loop, attachReq("alice", 0))
res, ok := outcome(t, worker)
require.True(t, ok, "a no-diff finalize must resolve promptly, not wait on a push")
require.NoError(t, res.Err)
assert.Equal(t, FinalizeAlreadyPresent, res.Outcome,
"a finalize that produces no diff resolves AlreadyPresent")
assert.Empty(t, res.SHA, "no commit was made, so there is no SHA")
}
// pushCompetingCommit advances the remote's main from a second clone, so a worker's
// next push conflicts and rebase-replays.
func pushCompetingCommit(t *testing.T, remoteURL string) {
t.Helper()
dir := t.TempDir()
repo, worktree := initLocalRepo(t, dir, remoteURL, "main")
commitFileChange(t, worktree, dir, "competing.txt", "from another writer\n")
require.NoError(t, repo.Push(&gogit.PushOptions{
RefSpecs: []config.RefSpec{config.RefSpec("refs/heads/main:refs/heads/main")},
}))
}
// TestAttach_ResyncCutOffCarriesMessageAndResolvesOnPush is the §8.3 intent-
// durability pin: a resync that cuts an Attached window before its deadline still
// commits the user's message, and the request resolves Committed once that
// carrying write is pushed — with the SHA actually on the remote.
func TestAttach_ResyncCutOffCarriesMessageAndResolvesOnPush(t *testing.T) {
worker, serverRepo, _ := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
loop := newBranchWorkerEventLoop(worker, time.Hour)
defer loop.stopTimers()
// Open alice's window with one edit.
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("held", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
require.NotNil(t, loop.openWindow)
// Attach with a distinctive message and a long grace, so the window is Attached
// but its finalize deadline has not fired.
const message = "save: intent must survive a resync"
req := attachReq("alice", 300)
req.Message = message
serviceAttach(loop, req)
require.NotNil(t, loop.openWindow.pendingCR, "the window must be attached")
_, resolved := outcome(t, worker)
require.False(t, resolved, "the request must not resolve before its window is finalized")
// Before the deadline, a resync for a different type cuts the window
// (resync-before-apply). The cut commit must carry the attached message.
scope := ResyncScope{GVR: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}}
resultCh := make(chan ResyncResult, 1)
loop.handleResyncRequest(&ResyncRequest{
GitTargetName: "team-a",
GitTargetNamespace: "default",
Scope: &scope,
Result: resultCh,
})
require.NoError(t, (<-resultCh).Err)
res, ok := outcome(t, worker)
require.True(t, ok, "the cut-off commit's push must resolve the request")
require.NoError(t, res.Err)
assert.Equal(t, FinalizeCommitted, res.Outcome)
require.NotEmpty(t, res.SHA)
// The commit on the remote carries the user's message verbatim (not the generated
// grouped message), and its SHA equals the reported SHA.
ref, err := serverRepo.Reference(plumbing.NewBranchReferenceName("main"), true)
require.NoError(t, err)
assert.Equal(t, ref.Hash().String(), res.SHA, "the reported SHA must be the commit on the remote")
commit, err := serverRepo.CommitObject(ref.Hash())
require.NoError(t, err)
assert.Equal(t, message, commit.Message, "the cut-off commit must carry the user's message verbatim")
}
// TestAttach_ConflictReplayResolvesToPostReplaySHA is the §8.3 complementary pin:
// when the push hits a conflict and rebase-replays, the request resolves to the
// post-replay commit actually on the remote — never a stale pre-rebase hash.
func TestAttach_ConflictReplayResolvesToPostReplaySHA(t *testing.T) {
worker, serverRepo, remoteURL := setupCommitPushSplitWorker(t)
createPlainGitTarget(t, worker, "team-a", "team-a")
// lastPushAt set so the finalize's push is deferred (a cooldown timer): the
// window commits locally, then we move the remote, then push explicitly.
loop := newBranchWorkerEventLoop(worker, time.Hour)
loop.lastPushAt = time.Now()
defer loop.stopTimers()
loop.handleQueueItem(WorkItem{Request: &WriteRequest{
Events: []Event{configMapTargetEvent("held", "alice", "team-a")},
CommitMode: CommitModePerEvent,
}})
const message = "save: survive a conflict replay"
req := attachReq("alice", 0)
req.Message = message
serviceAttach(loop, req) // finalize now; the push is deferred by the cooldown
require.Len(t, loop.pendingWrites, 1, "the window commit is retained, awaiting push")
_, resolved := outcome(t, worker)
require.False(t, resolved, "the request resolves on push, not at finalize")
localSHA := loop.pendingWrites[0].CommitSHA
require.False(t, localSHA.IsZero())
// A competing writer advances the remote, so the worker's push conflicts and
// rebase-replays onto the new base, producing a fresh commit hash.
pushCompetingCommit(t, remoteURL)
loop.pushPending()
require.Empty(t, loop.pendingWrites, "a successful replayed push clears the retained writes")
res, ok := outcome(t, worker)
require.True(t, ok)
require.NoError(t, res.Err)
assert.Equal(t, FinalizeCommitted, res.Outcome)
// The reported SHA is the POST-replay commit on the remote, not the stale
// pre-replay local hash.
ref, err := serverRepo.Reference(plumbing.NewBranchReferenceName("main"), true)
require.NoError(t, err)
assert.Equal(t, ref.Hash().String(), res.SHA, "the reported SHA must be the commit on the remote")
assert.NotEqual(t, localSHA.String(), res.SHA, "the SHA must be refreshed to the post-replay hash")
commit, err := serverRepo.CommitObject(ref.Hash())
require.NoError(t, err)
assert.Equal(t, message, commit.Message, "the replayed commit keeps the user's message")
}
// TestAttributionOutcome_ZeroValueIsNotAttempted pins the coupling the whole matching rule rests on:
// the zero value of AttributionOutcome must BE AttributionNotAttempted.
//
// Most producers of an Event never assign Attribution — reconcile, resync, bootstrap, and
// configured-author mode's early return in watch.Manager.attachAuthor all leave it zero, and all
// of them mean "no actor was sought". When this constant was "not_attempted" the zero value was a
// silent fourth state that compared equal to no named outcome, and no test caught it because
// every case set both sides to the zero value and compared "" to "".
func TestAttributionOutcome_ZeroValueIsNotAttempted(t *testing.T) {
var zero AttributionOutcome
assert.Equal(t, AttributionNotAttempted, zero,
"the zero value must be AttributionNotAttempted; three doc comments and every non-live "+
"event path depend on it")
assert.False(t, zero.NamesActor(), "an unset outcome names no actor")
assert.False(t, AttributionUnresolved.NamesActor(), "attribution ran and named nobody")
assert.True(t, AttributionResolved.NamesActor(), "only a resolved outcome names an actor")
}
// TestMatchesWindow_AcrossIndependentlyConfiguredSubsystems is the permanent regression test for
// the merge blocker: in the DEFAULT deployment no CommitRequest could attach to any window, so
// the user's commit message was silently dropped and the change landed under the generated
// message with no error anywhere.
//
// The window's outcome and the request's outcome come from two subsystems configured by two
// different flags (--author-attribution vs --admission-webhook, cmd/main.go:311-316), so this
// walks every pair either side can actually produce and names the deployment that produces it.
// TestCommitRequest_OutcomesAgree in internal/controller pins that these are the real
// values the two producers emit; here we pin what the matching rule does with them.
func TestMatchesWindow_AcrossIndependentlyConfiguredSubsystems(t *testing.T) {
const otherAuthor = "bob"
tests := []struct {
name string
deployment string
window AttributionOutcome
windowAuth string
request AttributionOutcome
reqAuth string
want bool
}{
{
name: "attribution off, webhook off",
deployment: "the DEFAULT deployment: --author-attribution=false, --admission-webhook=false",
window: AttributionNotAttempted, request: AttributionNotAttempted, want: true,
},
{
name: "attribution off, webhook on but missed",
deployment: "webhook enabled without Redis, or no admission record for the request",
window: AttributionNotAttempted, request: AttributionUnresolved, want: true,
},
{
name: "attribution on but unresolved, webhook off",
deployment: "attribution enabled, no audit fact within the grace; webhook off",
window: AttributionUnresolved, request: AttributionNotAttempted, want: true,
},
{
name: "attribution on but unresolved, webhook on but missed",
deployment: "both enabled, neither named an actor",
window: AttributionUnresolved, request: AttributionUnresolved, want: true,
},
{
name: "both resolved to the same actor",
deployment: "the fully configured deployment, request and window agree",
window: AttributionResolved, windowAuth: "alice",
request: AttributionResolved, reqAuth: "alice", want: true,
},
{
name: "both resolved to different actors",
deployment: "two humans editing the same GitTarget; must never cross-attach",
window: AttributionResolved, windowAuth: "alice",
request: AttributionResolved, reqAuth: otherAuthor, want: false,
},
{
name: "window named an actor, request did not",
deployment: "alice's window must not absorb an unattributable request",
window: AttributionResolved, windowAuth: "alice",
request: AttributionNotAttempted, want: false,
},
{
name: "request named an actor, window did not",
deployment: "alice's request must not claim an unattributable window",
window: AttributionNotAttempted,
request: AttributionResolved, reqAuth: "alice", want: false,
},
{
name: "different authors, neither outcome names an actor",
deployment: "not producible today (a set author implies a resolved outcome), but the " +
"author check must not become conditional on the outcome: if it did, any path that " +
"left an outcome unset while the author was set would let bob finalize alice's window",
window: AttributionNotAttempted, windowAuth: "alice",
request: AttributionNotAttempted, reqAuth: otherAuthor, want: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := &pendingCommitRequest{
author: tc.reqAuth,
attribution: tc.request,
gitTargetName: crTarget,
gitTargetNamespace: "default",
}
w := &openWindow{
Author: tc.windowAuth,
Attribution: tc.window,
GitTarget: crTarget,
GitTargetNamespace: "default",
}
assert.Equal(t, tc.want, p.matchesWindow(w), tc.deployment)
})
}
}
// TestMatchesWindow_GitTargetAlwaysScopes checks the GitTarget scope survives the outcome
// rework: no attribution pairing may let a request finalize another target's window.
func TestMatchesWindow_GitTargetAlwaysScopes(t *testing.T) {
for _, o := range []AttributionOutcome{AttributionNotAttempted, AttributionUnresolved, AttributionResolved} {
p := &pendingCommitRequest{attribution: o, gitTargetName: crTarget, gitTargetNamespace: "default"}
assert.False(t, p.matchesWindow(&openWindow{
Attribution: o, GitTarget: "team-b", GitTargetNamespace: "default",
}), "outcome %q must not match across GitTarget names", o)
assert.False(t, p.matchesWindow(&openWindow{
Attribution: o, GitTarget: crTarget, GitTargetNamespace: "other",
}), "outcome %q must not match across GitTarget namespaces", o)
}
}