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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 95 | 95 | {"pkg rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", false, false}, | |
| 96 | 96 | {"snapshot adapter accepts codec", modulePath + "/internal/adapters/snapshot", modulePath + "/internal/snapshotcodec", false, true}, | |
| 97 | 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 | 99 | {"snapshot codec rejects usecase", modulePath + "/internal/snapshotcodec", modulePath + "/internal/usecase/daemon", false, false}, | |
| 99 | 100 | {"snapshot codec rejects adapter", modulePath + "/internal/snapshotcodec", modulePath + "/internal/adapters/snapshot", false, false}, | |
| 100 | 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 | 226 | under := func(path, root string) bool { | |
| 226 | 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 | 234 | switch { | |
| 229 | 235 | case under(source, brokerPkg): | |
| 230 | 236 | 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 | |||
| 1129 | 1129 | ackStart = newACKBlockedSpan(c.opts.observer) | |
| 1130 | 1130 | 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 | 1133 | if !outputReady && !allDelivered { | |
| 1134 | 1134 | c.mu.Unlock() | |
| 1135 | 1135 | 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 = ackStart | 1130 | 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) { | |||
| 496 | 496 | func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) { | |
| 497 | 497 | t.Run("expired deadline flushes exactly once after readiness", func(t *testing.T) { | |
| 498 | 498 | h := newCoordinatorHarness(t) | |
| 499 | + | h.rc.attach(&attachedClient{}) | |
| 499 | 500 | h.ackReady.Store(false) | |
| 500 | 501 | h.rc.invalidate(renderInvalidation{class: invalidateOutput, reset: true, producer: "render.go"}) | |
| 501 | 502 | timer := awaitCoordinatorScheduledTimer(t, h.clk) | |
| @@ -516,6 +517,7 @@ func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) { | |||
| 516 | 517 | ||
| 517 | 518 | t.Run("lifecycle clears deferred work and urgent explicit fires stay immediate", func(t *testing.T) { | |
| 518 | 519 | h := newCoordinatorHarness(t) | |
| 520 | + | h.rc.attach(&attachedClient{}) | |
| 519 | 521 | h.ackReady.Store(false) | |
| 520 | 522 | h.rc.invalidate(renderInvalidation{class: invalidateOutput}) | |
| 521 | 523 | timer := awaitCoordinatorScheduledTimer(t, h.clk) | |
| @@ -536,6 +538,7 @@ func TestRenderCoordinatorAckFlushesOnlyExpiredAckDeferredWork(t *testing.T) { | |||
| 536 | 538 | ||
| 537 | 539 | func TestRenderCoordinatorAckGateBlocksCompositionUntilAck(t *testing.T) { | |
| 538 | 540 | h := newCoordinatorHarness(t) | |
| 541 | + | h.rc.attach(&attachedClient{}) | |
| 539 | 542 | h.ackReady.Store(false) | |
| 540 | 543 | ||
| 541 | 544 | 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}) | ||