detached 4efbe32

test(daemon): attach a client in render ACK-gate tests and narrow snapshot codec imports

The ACK-gate coordinator tests attach an attachment so the per-attachment
readiness probe gates output again; a lease-less coordinator is ungated.
The dead lease-less ackDeferred branch is removed. Only the snapshot
repository adapter may import internal/snapshotcodec among adapters.
commit
4efbe3208160a3f1397f7ed981947b36b57334c8
author
brice · · 2026-10-08 05:33 UTC
parent
5c38eb4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +10 -1
M boundary_test.go +6 -0
@@ -95,6 +95,7 @@ func TestImportBoundaryNegativeFixtures(t *testing.T) {
9595 {"pkg rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", false, false},
9696 {"snapshot adapter accepts codec", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/snapshotcodec", false, true},
9797 {"daemon accepts snapshot codec", modulePath + "/internal/usecase/daemon", modulePath + "/internal/snapshotcodec", false, true},
98+ {"other adapter rejects snapshot codec", modulePath + "/internal/adapters/ipc", modulePath + "/internal/snapshotcodec", false, false},
9899 {"snapshot codec rejects usecase", modulePath + "/internal/snapshotcodec", modulePath + "/internal/usecase/daemon", false, false},
99100 {"snapshot codec rejects adapter", modulePath + "/internal/snapshotcodec", modulePath + "/internal/adapters/snapshot", false, false},
100101 {"snapshot adapter rejects usecase", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/usecase/daemon", false, false},
@@ -225,6 +226,11 @@ func packageImportDenied(source, target string) bool {
225226 under := func(path, root string) bool {
226227 return path == root || strings.HasPrefix(path, root+"/")
227228 }
229+ // The durable snapshot format is shared by the daemon and its one
230+ // repository adapter; no other adapter may depend on it.
231+ if target == modulePath+"/internal/snapshotcodec" && strings.HasPrefix(source, modulePath+"/internal/adapters/") {
232+ return source != modulePath+"/internal/adapters/snapshot"
233+ }
228234 switch {
229235 case under(source, brokerPkg):
230236 return !under(target, brokerPkg) && strings.HasPrefix(target, usecasePrefix)
@@ -95,6 +95,7 @@ func TestImportBoundaryNegativeFixtures(t *testing.T) {
95 {"pkg rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", false, false},95 {"pkg rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", false, false},
96 {"snapshot adapter accepts codec", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/snapshotcodec", false, true},96 {"snapshot adapter accepts codec", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/snapshotcodec", false, true},
97 {"daemon accepts snapshot codec", modulePath + "/internal/usecase/daemon", modulePath + "/internal/snapshotcodec", false, true},97 {"daemon accepts snapshot codec", modulePath + "/internal/usecase/daemon", modulePath + "/internal/snapshotcodec", false, true},
98+ {"other adapter rejects snapshot codec", modulePath + "/internal/adapters/ipc", modulePath + "/internal/snapshotcodec", false, false},
98 {"snapshot codec rejects usecase", modulePath + "/internal/snapshotcodec", modulePath + "/internal/usecase/daemon", false, false},99 {"snapshot codec rejects usecase", modulePath + "/internal/snapshotcodec", modulePath + "/internal/usecase/daemon", false, false},
99 {"snapshot codec rejects adapter", modulePath + "/internal/snapshotcodec", modulePath + "/internal/adapters/snapshot", false, false},100 {"snapshot codec rejects adapter", modulePath + "/internal/snapshotcodec", modulePath + "/internal/adapters/snapshot", false, false},
100 {"snapshot adapter rejects usecase", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/usecase/daemon", false, false},101 {"snapshot adapter rejects usecase", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/usecase/daemon", false, false},
@@ -225,6 +226,11 @@ func packageImportDenied(source, target string) bool {
225 under := func(path, root string) bool {226 under := func(path, root string) bool {
226 return path == root || strings.HasPrefix(path, root+"/")227 return path == root || strings.HasPrefix(path, root+"/")
227 }228 }
229+ // The durable snapshot format is shared by the daemon and its one
230+ // repository adapter; no other adapter may depend on it.
231+ if target == modulePath+"/internal/snapshotcodec" && strings.HasPrefix(source, modulePath+"/internal/adapters/") {
232+ return source != modulePath+"/internal/adapters/snapshot"
233+ }
228 switch {234 switch {
229 case under(source, brokerPkg):235 case under(source, brokerPkg):
230 return !under(target, brokerPkg) && strings.HasPrefix(target, usecasePrefix)236 return !under(target, brokerPkg) && strings.HasPrefix(target, usecasePrefix)
M internal/usecase/daemon/render_coordinator.go +1 -1
@@ -1129,7 +1129,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui
11291129 ackStart = newACKBlockedSpan(c.opts.observer)
11301130 c.ackBlocked = ackStart
11311131 }
1132- c.ackDeferred = len(c.ackDeferredFor) != 0 || (len(leases) == 0 && blockedByAck && deadline)
1132+ c.ackDeferred = len(c.ackDeferredFor) != 0
11331133 if !outputReady && !allDelivered {
11341134 c.mu.Unlock()
11351135 stopDetachedTimer(worker)
@@ -1129,7 +1129,7 @@ func (c *renderCoordinator) fireWithTimerTokenAndLease(token *timerToken, gen ui
1129 ackStart = newACKBlockedSpan(c.opts.observer)1129 ackStart = newACKBlockedSpan(c.opts.observer)
1130 c.ackBlocked = ackStart1130 c.ackBlocked = ackStart
1131 }1131 }
1132- c.ackDeferred = len(c.ackDeferredFor) != 0 || (len(leases) == 0 && blockedByAck && deadline)1132+ c.ackDeferred = len(c.ackDeferredFor) != 0
1133 if !outputReady && !allDelivered {1133 if !outputReady && !allDelivered {
1134 c.mu.Unlock()1134 c.mu.Unlock()
1135 stopDetachedTimer(worker)1135 stopDetachedTimer(worker)
M internal/usecase/daemon/render_coordinator_test.go +3 -0
@@ -496,6 +496,7 @@ func TestRenderCoordinatorAckDoesNotBypassAnUnexpiredDeadline(t *testing.T) {
496496 func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) {
497497 t.Run("expired deadline flushes exactly once after readiness", func(t *testing.T) {
498498 h := newCoordinatorHarness(t)
499+ h.rc.attach(&attachedClient{})
499500 h.ackReady.Store(false)
500501 h.rc.invalidate(renderInvalidation{class: invalidateOutput, reset: true, producer: "render.go"})
501502 timer := awaitCoordinatorScheduledTimer(t, h.clk)
@@ -516,6 +517,7 @@ func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) {
516517
517518 t.Run("lifecycle clears deferred work and urgent explicit fires stay immediate", func(t *testing.T) {
518519 h := newCoordinatorHarness(t)
520+ h.rc.attach(&attachedClient{})
519521 h.ackReady.Store(false)
520522 h.rc.invalidate(renderInvalidation{class: invalidateOutput})
521523 timer := awaitCoordinatorScheduledTimer(t, h.clk)
@@ -536,6 +538,7 @@ func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) {
536538
537539 func TestRenderCoordinatorAckGateBlocksCompositionUntilAck(t *testing.T) {
538540 h := newCoordinatorHarness(t)
541+ h.rc.attach(&attachedClient{})
539542 h.ackReady.Store(false)
540543
541544 h.rc.invalidate(renderInvalidation{class: invalidateOutput})
@@ -496,6 +496,7 @@ func TestRenderCoordinatorAckDoesNotBypassAnUnexpiredDeadline(t *testing.T) {
496 func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) {496 func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) {
497 t.Run("expired deadline flushes exactly once after readiness", func(t *testing.T) {497 t.Run("expired deadline flushes exactly once after readiness", func(t *testing.T) {
498 h := newCoordinatorHarness(t)498 h := newCoordinatorHarness(t)
499+ h.rc.attach(&attachedClient{})
499 h.ackReady.Store(false)500 h.ackReady.Store(false)
500 h.rc.invalidate(renderInvalidation{class: invalidateOutput, reset: true, producer: "render.go"})501 h.rc.invalidate(renderInvalidation{class: invalidateOutput, reset: true, producer: "render.go"})
501 timer := awaitCoordinatorScheduledTimer(t, h.clk)502 timer := awaitCoordinatorScheduledTimer(t, h.clk)
@@ -516,6 +517,7 @@ func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) {
516 517
517 t.Run("lifecycle clears deferred work and urgent explicit fires stay immediate", func(t *testing.T) {518 t.Run("lifecycle clears deferred work and urgent explicit fires stay immediate", func(t *testing.T) {
518 h := newCoordinatorHarness(t)519 h := newCoordinatorHarness(t)
520+ h.rc.attach(&attachedClient{})
519 h.ackReady.Store(false)521 h.ackReady.Store(false)
520 h.rc.invalidate(renderInvalidation{class: invalidateOutput})522 h.rc.invalidate(renderInvalidation{class: invalidateOutput})
521 timer := awaitCoordinatorScheduledTimer(t, h.clk)523 timer := awaitCoordinatorScheduledTimer(t, h.clk)
@@ -536,6 +538,7 @@ func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) {
536 538
537 func TestRenderCoordinatorAckGateBlocksCompositionUntilAck(t *testing.T) {539 func TestRenderCoordinatorAckGateBlocksCompositionUntilAck(t *testing.T) {
538 h := newCoordinatorHarness(t)540 h := newCoordinatorHarness(t)
541+ h.rc.attach(&attachedClient{})
539 h.ackReady.Store(false)542 h.ackReady.Store(false)
540 543
541 h.rc.invalidate(renderInvalidation{class: invalidateOutput})544 h.rc.invalidate(renderInvalidation{class: invalidateOutput})

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close