feat(T3.4): implement human-in-the-loop approval gates
- Add internal/approval package for workflow approval gates - Implement ApprovalGate for gating workflow progression - Implement ApprovalGateManager for managing multiple gates - Gate status tracking: pending, approved, rejected, expired - TTL-based gate expiration (auto-expire after timeout) - Multiple approval tracking (configurable approval count) - History tracking for all approval decisions - Query by task, workflow, status - Audit trail with decision reasons - 16 approval tests, all passing
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package approval
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewApprovalGateManager(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
assert.NotNil(t, manager)
|
||||
assert.Equal(t, 0, manager.stats.TotalGates)
|
||||
}
|
||||
|
||||
func TestCreateGate(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
assert.NotNil(t, gate)
|
||||
assert.Equal(t, "T0.1", gate.TaskID)
|
||||
assert.Equal(t, StatusPending, gate.Decision.Status)
|
||||
}
|
||||
|
||||
func TestApproveGate(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
err := manager.ApproveGate(gate.ID, "reviewer-1", "Approved")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, manager.IsApproved(gate.ID))
|
||||
}
|
||||
|
||||
func TestRejectGate(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
err := manager.RejectGate(gate.ID, "reviewer-1", "Rejected")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, manager.IsRejected(gate.ID))
|
||||
}
|
||||
|
||||
func TestGetPendingGates(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate1 := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
gate2 := manager.CreateGate("T0.2", "workflow-1", "Review", 24*time.Hour)
|
||||
|
||||
manager.ApproveGate(gate1.ID, "reviewer-1", "")
|
||||
|
||||
pending := manager.GetPendingGates()
|
||||
assert.Equal(t, 1, len(pending))
|
||||
if len(pending) > 0 {
|
||||
assert.Equal(t, gate2.ID, pending[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGatesByTask(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
manager.CreateGate("T0.2", "workflow-1", "Review", 24*time.Hour)
|
||||
|
||||
gates := manager.GetGatesByTask("T0.1")
|
||||
assert.Greater(t, len(gates), 0)
|
||||
}
|
||||
|
||||
func TestGetGatesByWorkflow(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
manager.CreateGate("T0.2", "workflow-2", "Review", 24*time.Hour)
|
||||
|
||||
gates := manager.GetGatesByWorkflow("workflow-1")
|
||||
assert.Greater(t, len(gates), 0)
|
||||
}
|
||||
|
||||
func TestIsApproved(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
assert.False(t, manager.IsApproved(gate.ID))
|
||||
|
||||
manager.ApproveGate(gate.ID, "reviewer-1", "")
|
||||
assert.True(t, manager.IsApproved(gate.ID))
|
||||
}
|
||||
|
||||
func TestIsRejected(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
assert.False(t, manager.IsRejected(gate.ID))
|
||||
|
||||
manager.RejectGate(gate.ID, "reviewer-1", "")
|
||||
assert.True(t, manager.IsRejected(gate.ID))
|
||||
}
|
||||
|
||||
func TestIsPending(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
assert.True(t, manager.IsPending(gate.ID))
|
||||
|
||||
manager.ApproveGate(gate.ID, "reviewer-1", "")
|
||||
assert.False(t, manager.IsPending(gate.ID))
|
||||
}
|
||||
|
||||
func TestGetStats(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate1 := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
_ = manager.CreateGate("T0.2", "workflow-1", "Review", 24*time.Hour)
|
||||
|
||||
manager.ApproveGate(gate1.ID, "reviewer-1", "")
|
||||
|
||||
stats := manager.GetStats()
|
||||
assert.Equal(t, 2, stats.TotalGates)
|
||||
assert.Equal(t, 1, stats.ApprovedGates)
|
||||
assert.Equal(t, 1, stats.PendingGates)
|
||||
}
|
||||
|
||||
func TestGetHistory(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate1 := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
gate2 := manager.CreateGate("T0.2", "workflow-1", "Review", 24*time.Hour)
|
||||
|
||||
manager.ApproveGate(gate1.ID, "reviewer-1", "")
|
||||
manager.RejectGate(gate2.ID, "reviewer-2", "Needs work")
|
||||
|
||||
history := manager.GetHistory()
|
||||
assert.Greater(t, len(history), 0)
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
assert.Equal(t, 1, manager.stats.TotalGates)
|
||||
|
||||
manager.Clear()
|
||||
assert.Equal(t, 0, manager.stats.TotalGates)
|
||||
}
|
||||
|
||||
func TestExpiredGate(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 1*time.Millisecond)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
err := manager.ApproveGate(gate.ID, "reviewer-1", "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMultipleApprovals(t *testing.T) {
|
||||
manager := NewApprovalGateManager()
|
||||
gate := manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
gate.RequiredApprovals = 2
|
||||
|
||||
err := manager.ApproveGate(gate.ID, "reviewer-1", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
retrieved, _ := manager.GetGate(gate.ID)
|
||||
assert.Equal(t, 1, len(retrieved.Approvals))
|
||||
}
|
||||
|
||||
func BenchmarkCreateGate(b *testing.B) {
|
||||
manager := NewApprovalGateManager()
|
||||
for i := 0; i < b.N; i++ {
|
||||
manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApproveGate(b *testing.B) {
|
||||
manager := NewApprovalGateManager()
|
||||
gates := make([]*ApprovalGate, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
gates[i] = manager.CreateGate("T0.1", "workflow-1", "Review", 24*time.Hour)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
manager.ApproveGate(gates[i].ID, "reviewer-1", "")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user