proxy-pool/internal/domain/workerruntime/contracttest/contract.go
youfak a79d030c82
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: add worker session runtime domain store
2026-07-31 10:57:39 +08:00

132 lines
4.9 KiB
Go

package contracttest
import (
"context"
"crypto/sha256"
"errors"
"testing"
"time"
"proxy-pool/internal/domain/workerruntime"
)
type Fixture struct {
Store workerruntime.ControlStore
Reader workerruntime.RuntimeReader
Advance func(time.Duration)
}
type Factory func(*testing.T) Fixture
// Run exercises the public control-store behavior shared by Memory and Redis.
func Run(t *testing.T, factory Factory) {
t.Helper()
t.Run("acknowledged runtime lifecycle", func(t *testing.T) { runLifecycle(t, newFixture(t, factory)) })
t.Run("negative acknowledgement fences runtime", func(t *testing.T) { runNegativeAck(t, newFixture(t, factory)) })
}
func runLifecycle(t *testing.T, fixture Fixture) {
t.Helper()
ctx := context.Background()
open(t, fixture.Store)
epoch, err := fixture.Store.CurrentOwnershipEpoch(ctx)
if err != nil {
t.Fatalf("CurrentOwnershipEpoch(): %v", err)
}
reference := snapshot(7, epoch, "snapshot-7")
report := runtimeReport(1, 7, epoch)
if err := fixture.Store.ReplaceRuntime(ctx, report, time.Minute); !errors.Is(err, workerruntime.ErrSnapshotMismatch) {
t.Fatalf("ReplaceRuntime(before ACK) error = %v", err)
}
if err := fixture.Store.RecordIssuedSnapshot(ctx, reference, time.Minute); err != nil {
t.Fatalf("RecordIssuedSnapshot(): %v", err)
}
ack := workerruntime.SnapshotAcknowledgement{WorkerID: "worker-a", SessionID: "session-a", Reference: reference, Applied: true}
if err := fixture.Store.AcknowledgeSnapshot(ctx, ack, time.Minute); err != nil {
t.Fatalf("AcknowledgeSnapshot(): %v", err)
}
if err := fixture.Store.ReplaceRuntime(ctx, report, time.Minute); err != nil {
t.Fatalf("ReplaceRuntime(after ACK): %v", err)
}
assertFresh(t, fixture.Reader, epoch, true)
if err := fixture.Store.AcknowledgeSnapshot(ctx, ack, time.Minute); err != nil {
t.Fatalf("AcknowledgeSnapshot(replay): %v", err)
}
if err := fixture.Store.ReplaceRuntime(ctx, runtimeReport(0, 7, epoch), time.Minute); !errors.Is(err, workerruntime.ErrInvalidReport) {
t.Fatalf("ReplaceRuntime(invalid sequence) error = %v", err)
}
fixture.Advance(2 * time.Minute)
assertFresh(t, fixture.Reader, epoch, false)
}
func runNegativeAck(t *testing.T, fixture Fixture) {
t.Helper()
ctx := context.Background()
open(t, fixture.Store)
epoch, err := fixture.Store.CurrentOwnershipEpoch(ctx)
if err != nil {
t.Fatalf("CurrentOwnershipEpoch(): %v", err)
}
first := snapshot(7, epoch, "snapshot-7")
if err := fixture.Store.RecordIssuedSnapshot(ctx, first, time.Minute); err != nil {
t.Fatalf("RecordIssuedSnapshot(first): %v", err)
}
if err := fixture.Store.AcknowledgeSnapshot(ctx, workerruntime.SnapshotAcknowledgement{WorkerID: "worker-a", SessionID: "session-a", Reference: first, Applied: true}, time.Minute); err != nil {
t.Fatalf("AcknowledgeSnapshot(first): %v", err)
}
second := snapshot(8, epoch, "snapshot-8")
if err := fixture.Store.RecordIssuedSnapshot(ctx, second, time.Minute); err != nil {
t.Fatalf("RecordIssuedSnapshot(second): %v", err)
}
if err := fixture.Store.AcknowledgeSnapshot(ctx, workerruntime.SnapshotAcknowledgement{
WorkerID: "worker-a", SessionID: "session-a", Reference: second, ErrorCode: "apply_failed",
}, time.Minute); err != nil {
t.Fatalf("AcknowledgeSnapshot(negative): %v", err)
}
if err := fixture.Store.ReplaceRuntime(ctx, runtimeReport(1, 7, epoch), time.Minute); !errors.Is(err, workerruntime.ErrSnapshotMismatch) {
t.Fatalf("ReplaceRuntime(delayed): %v", err)
}
}
func newFixture(t *testing.T, factory Factory) Fixture {
t.Helper()
fixture := factory(t)
if fixture.Store == nil || fixture.Reader == nil || fixture.Advance == nil {
t.Fatal("contract fixture is incomplete")
}
return fixture
}
func open(t *testing.T, store workerruntime.ControlStore) {
t.Helper()
err := store.OpenSession(context.Background(), workerruntime.Session{
WorkerID: "worker-a", InstanceID: "instance-a", SessionID: "session-a", Zone: "zone-a", ProtocolVersion: 1,
}, time.Minute)
if err != nil {
t.Fatalf("OpenSession(): %v", err)
}
}
func snapshot(version, epoch uint64, value string) workerruntime.SnapshotReference {
return workerruntime.SnapshotReference{
WorkerID: "worker-a", Version: version, OwnershipEpoch: epoch, Checksum: sha256.Sum256([]byte(value)),
}
}
func runtimeReport(sequence, version, epoch uint64) workerruntime.Report {
return workerruntime.Report{
WorkerID: "worker-a", SessionID: "session-a", Sequence: sequence, SnapshotVersion: version,
OwnershipEpoch: epoch, ObservedAt: time.Date(2026, 7, 31, 9, 0, 0, 0, time.UTC),
}
}
func assertFresh(t *testing.T, reader workerruntime.RuntimeReader, epoch uint64, want bool) {
t.Helper()
snapshots, err := reader.ReadRuntime(context.Background(), []workerruntime.OwnedProxy{{
ProxyID: "proxy-a", WorkerID: "worker-a", OwnershipEpoch: epoch,
}})
if err != nil || len(snapshots) != 1 || snapshots[0].Fresh != want {
t.Fatalf("ReadRuntime() = %+v, %v; want Fresh=%t", snapshots, err, want)
}
}