1129 lines
32 KiB
Go
1129 lines
32 KiB
Go
/*
|
|
* Copyright 2025 CloudWeGo Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package core
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// Define AddressSegmentType constants locally to avoid dependency cycles
|
|
const (
|
|
AddressSegmentAgent AddressSegmentType = "agent"
|
|
AddressSegmentTool AddressSegmentType = "tool"
|
|
AddressSegmentNode AddressSegmentType = "node"
|
|
)
|
|
|
|
func TestInterruptConversion(t *testing.T) {
|
|
// Test Case 1: Simple Chain (A -> B -> C)
|
|
t.Run("SimpleChain", func(t *testing.T) {
|
|
// Manually construct the user-facing contexts with parent pointers
|
|
ctxA := &InterruptCtx{ID: "A", IsRootCause: false}
|
|
ctxB := &InterruptCtx{ID: "B", Parent: ctxA, IsRootCause: false}
|
|
ctxC := &InterruptCtx{ID: "C", Parent: ctxB, IsRootCause: true}
|
|
|
|
// The input to FromInterruptContexts is just the root cause leaf node
|
|
contexts := []*InterruptCtx{ctxC}
|
|
|
|
// Convert from user-facing contexts to internal signal tree
|
|
signal := FromInterruptContexts(contexts)
|
|
|
|
// Assertions for the signal tree structure
|
|
assert.NotNil(t, signal)
|
|
assert.Equal(t, "A", signal.ID)
|
|
assert.Len(t, signal.Subs, 1)
|
|
assert.Equal(t, "B", signal.Subs[0].ID)
|
|
assert.Len(t, signal.Subs[0].Subs, 1)
|
|
assert.Equal(t, "C", signal.Subs[0].Subs[0].ID)
|
|
assert.True(t, signal.Subs[0].Subs[0].IsRootCause)
|
|
|
|
// Convert back from the signal tree to user-facing contexts
|
|
finalContexts := ToInterruptContexts(signal, nil)
|
|
|
|
// Assertions for the final list of contexts
|
|
assert.Len(t, finalContexts, 1)
|
|
finalC := finalContexts[0]
|
|
assert.Equal(t, "C", finalC.ID)
|
|
assert.True(t, finalC.IsRootCause)
|
|
assert.NotNil(t, finalC.Parent)
|
|
assert.Equal(t, "B", finalC.Parent.ID)
|
|
assert.NotNil(t, finalC.Parent.Parent)
|
|
assert.Equal(t, "A", finalC.Parent.Parent.ID)
|
|
assert.Nil(t, finalC.Parent.Parent.Parent)
|
|
})
|
|
|
|
// Test Case 2: Multiple Root Causes with Shared Parent (B -> D, C -> D)
|
|
t.Run("MultipleRootsSharedParent", func(t *testing.T) {
|
|
// Manually construct the contexts
|
|
ctxD := &InterruptCtx{ID: "D", IsRootCause: false}
|
|
ctxB := &InterruptCtx{ID: "B", Parent: ctxD, IsRootCause: true}
|
|
ctxC := &InterruptCtx{ID: "C", Parent: ctxD, IsRootCause: true}
|
|
|
|
// The input contains both root cause leaves
|
|
contexts := []*InterruptCtx{ctxB, ctxC}
|
|
|
|
// Convert to signal tree
|
|
signal := FromInterruptContexts(contexts)
|
|
|
|
// Assertions for the signal tree structure (should merge at D)
|
|
assert.NotNil(t, signal)
|
|
assert.Equal(t, "D", signal.ID)
|
|
assert.Len(t, signal.Subs, 2)
|
|
// Order of subs is not guaranteed, so we check for presence
|
|
subIDs := []string{signal.Subs[0].ID, signal.Subs[1].ID}
|
|
assert.Contains(t, subIDs, "B")
|
|
assert.Contains(t, subIDs, "C")
|
|
|
|
// Convert back to user-facing contexts
|
|
finalContexts := ToInterruptContexts(signal, nil)
|
|
|
|
// Assertions for the final list of contexts
|
|
assert.Len(t, finalContexts, 2)
|
|
finalIDs := []string{finalContexts[0].ID, finalContexts[1].ID}
|
|
assert.Contains(t, finalIDs, "B")
|
|
assert.Contains(t, finalIDs, "C")
|
|
|
|
// Check parent linking for one of the branches
|
|
var finalB *InterruptCtx
|
|
if finalContexts[0].ID == "B" {
|
|
finalB = finalContexts[0]
|
|
} else {
|
|
finalB = finalContexts[1]
|
|
}
|
|
assert.NotNil(t, finalB.Parent)
|
|
assert.Equal(t, "D", finalB.Parent.ID)
|
|
assert.Nil(t, finalB.Parent.Parent)
|
|
})
|
|
|
|
// Test Case 3: Nil and Empty Inputs
|
|
t.Run("NilAndEmpty", func(t *testing.T) {
|
|
assert.Nil(t, FromInterruptContexts(nil))
|
|
assert.Nil(t, FromInterruptContexts([]*InterruptCtx{}))
|
|
assert.Nil(t, ToInterruptContexts(nil, nil))
|
|
})
|
|
}
|
|
|
|
func TestSignalToPersistenceMaps(t *testing.T) {
|
|
// Test Case 1: Nil Signal
|
|
t.Run("NilSignal", func(t *testing.T) {
|
|
id2addr, id2state := SignalToPersistenceMaps(nil)
|
|
assert.NotNil(t, id2addr)
|
|
assert.NotNil(t, id2state)
|
|
assert.Empty(t, id2addr)
|
|
assert.Empty(t, id2state)
|
|
})
|
|
|
|
// Test Case 2: Single Node Signal
|
|
t.Run("SingleNode", func(t *testing.T) {
|
|
signal := &InterruptSignal{
|
|
ID: "node1",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "test state",
|
|
LayerSpecificPayload: "test payload",
|
|
},
|
|
}
|
|
|
|
id2addr, id2state := SignalToPersistenceMaps(signal)
|
|
|
|
assert.Len(t, id2addr, 1)
|
|
assert.Len(t, id2state, 1)
|
|
|
|
assert.Equal(t, signal.Address, id2addr["node1"])
|
|
assert.Equal(t, signal.InterruptState, id2state["node1"])
|
|
})
|
|
|
|
// Test Case 3: Simple Tree Structure
|
|
t.Run("SimpleTree", func(t *testing.T) {
|
|
child1 := &InterruptSignal{
|
|
ID: "child1",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "child1 state",
|
|
},
|
|
}
|
|
|
|
child2 := &InterruptSignal{
|
|
ID: "child2",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool2"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "child2 state",
|
|
},
|
|
}
|
|
|
|
parent := &InterruptSignal{
|
|
ID: "parent",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "parent state",
|
|
},
|
|
Subs: []*InterruptSignal{child1, child2},
|
|
}
|
|
|
|
id2addr, id2state := SignalToPersistenceMaps(parent)
|
|
|
|
// Should contain all 3 nodes
|
|
assert.Len(t, id2addr, 3)
|
|
assert.Len(t, id2state, 3)
|
|
|
|
// Check parent node
|
|
assert.Equal(t, parent.Address, id2addr["parent"])
|
|
assert.Equal(t, parent.InterruptState, id2state["parent"])
|
|
|
|
// Check child nodes
|
|
assert.Equal(t, child1.Address, id2addr["child1"])
|
|
assert.Equal(t, child1.InterruptState, id2state["child1"])
|
|
assert.Equal(t, child2.Address, id2addr["child2"])
|
|
assert.Equal(t, child2.InterruptState, id2state["child2"])
|
|
})
|
|
|
|
// Test Case 4: Deeply Nested Tree
|
|
t.Run("DeeplyNestedTree", func(t *testing.T) {
|
|
leaf1 := &InterruptSignal{
|
|
ID: "leaf1",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
{Type: AddressSegmentNode, ID: "node1"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "leaf1 state",
|
|
},
|
|
}
|
|
|
|
leaf2 := &InterruptSignal{
|
|
ID: "leaf2",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
{Type: AddressSegmentNode, ID: "node2"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "leaf2 state",
|
|
},
|
|
}
|
|
|
|
middle := &InterruptSignal{
|
|
ID: "middle",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "middle state",
|
|
},
|
|
Subs: []*InterruptSignal{leaf1, leaf2},
|
|
}
|
|
|
|
root := &InterruptSignal{
|
|
ID: "root",
|
|
Address: Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "root state",
|
|
},
|
|
Subs: []*InterruptSignal{middle},
|
|
}
|
|
|
|
id2addr, id2state := SignalToPersistenceMaps(root)
|
|
|
|
// Should contain all 4 nodes
|
|
assert.Len(t, id2addr, 4)
|
|
assert.Len(t, id2state, 4)
|
|
|
|
// Verify all nodes are present
|
|
assert.Equal(t, root.Address, id2addr["root"])
|
|
assert.Equal(t, root.InterruptState, id2state["root"])
|
|
assert.Equal(t, middle.Address, id2addr["middle"])
|
|
assert.Equal(t, middle.InterruptState, id2state["middle"])
|
|
assert.Equal(t, leaf1.Address, id2addr["leaf1"])
|
|
assert.Equal(t, leaf1.InterruptState, id2state["leaf1"])
|
|
assert.Equal(t, leaf2.Address, id2addr["leaf2"])
|
|
assert.Equal(t, leaf2.InterruptState, id2state["leaf2"])
|
|
})
|
|
|
|
// Test Case 5: Complex Tree with Multiple Branches
|
|
t.Run("ComplexTree", func(t *testing.T) {
|
|
// Create a complex tree structure with multiple branches
|
|
branch1Leaf1 := &InterruptSignal{ID: "b1l1", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b1l1"}}
|
|
branch1Leaf2 := &InterruptSignal{ID: "b1l2", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b1l2"}}
|
|
branch1 := &InterruptSignal{ID: "b1", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b1"}, Subs: []*InterruptSignal{branch1Leaf1, branch1Leaf2}}
|
|
|
|
branch2Leaf1 := &InterruptSignal{ID: "b2l1", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b2l1"}}
|
|
branch2 := &InterruptSignal{ID: "b2", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "b2"}, Subs: []*InterruptSignal{branch2Leaf1}}
|
|
|
|
root := &InterruptSignal{ID: "root", Address: Address{{Type: AddressSegmentAgent, ID: "a1"}}, InterruptState: InterruptState{State: "root"}, Subs: []*InterruptSignal{branch1, branch2}}
|
|
|
|
id2addr, id2state := SignalToPersistenceMaps(root)
|
|
|
|
// Should contain all 6 nodes
|
|
assert.Len(t, id2addr, 6)
|
|
assert.Len(t, id2state, 6)
|
|
|
|
// Verify all nodes are present
|
|
expectedNodes := []string{"root", "b1", "b2", "b1l1", "b1l2", "b2l1"}
|
|
for _, nodeID := range expectedNodes {
|
|
assert.Contains(t, id2addr, nodeID)
|
|
assert.Contains(t, id2state, nodeID)
|
|
}
|
|
})
|
|
|
|
// Test Case 6: Empty InterruptState Values
|
|
t.Run("EmptyInterruptState", func(t *testing.T) {
|
|
signal := &InterruptSignal{
|
|
ID: "node1",
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
InterruptState: InterruptState{
|
|
// Empty state values
|
|
},
|
|
}
|
|
|
|
id2addr, id2state := SignalToPersistenceMaps(signal)
|
|
|
|
assert.Len(t, id2addr, 1)
|
|
assert.Len(t, id2state, 1)
|
|
assert.Equal(t, signal.Address, id2addr["node1"])
|
|
assert.Equal(t, signal.InterruptState, id2state["node1"])
|
|
})
|
|
}
|
|
|
|
func TestGetCurrentAddress(t *testing.T) {
|
|
// Test Case 1: No Address in Context
|
|
t.Run("NoAddressInContext", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
addr := GetCurrentAddress(ctx)
|
|
assert.Nil(t, addr)
|
|
})
|
|
|
|
// Test Case 2: Address in Context
|
|
t.Run("AddressInContext", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
expectedAddr := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
}
|
|
|
|
// Create a context with address using internal addrCtx
|
|
runCtx := &addrCtx{
|
|
addr: expectedAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
addr := GetCurrentAddress(ctx)
|
|
assert.Equal(t, expectedAddr, addr)
|
|
})
|
|
}
|
|
|
|
func TestGetNextResumptionPoints(t *testing.T) {
|
|
// Test Case 1: No Resume Info in Context
|
|
t.Run("NoResumeInfo", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
_, err := GetNextResumptionPoints(ctx)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to get resume info")
|
|
})
|
|
|
|
// Test Case 2: Empty Resume Info
|
|
t.Run("EmptyResumeInfo", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
rInfo := &globalResumeInfo{
|
|
id2Addr: make(map[string]Address),
|
|
}
|
|
ctx = context.WithValue(ctx, globalResumeInfoKey{}, rInfo)
|
|
|
|
points, err := GetNextResumptionPoints(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, points)
|
|
})
|
|
|
|
// Test Case 3: Valid Resume Points
|
|
t.Run("ValidResumePoints", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Set up current address
|
|
currentAddr := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
}
|
|
runCtx := &addrCtx{
|
|
addr: currentAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
// Set up resume info with child addresses
|
|
rInfo := &globalResumeInfo{
|
|
id2Addr: map[string]Address{
|
|
"child1": {
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
},
|
|
"child2": {
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool2"},
|
|
},
|
|
"unrelated": {
|
|
{Type: AddressSegmentAgent, ID: "agent2"},
|
|
},
|
|
},
|
|
}
|
|
ctx = context.WithValue(ctx, globalResumeInfoKey{}, rInfo)
|
|
|
|
points, err := GetNextResumptionPoints(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, points, 2)
|
|
assert.True(t, points["tool1"])
|
|
assert.True(t, points["tool2"])
|
|
})
|
|
|
|
// Test Case 4: Root Address (Empty Parent)
|
|
t.Run("RootAddress", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Empty current address (root)
|
|
runCtx := &addrCtx{
|
|
addr: Address{},
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
// Set up resume info with various addresses
|
|
rInfo := &globalResumeInfo{
|
|
id2Addr: map[string]Address{
|
|
"agent1": {
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
},
|
|
"agent2": {
|
|
{Type: AddressSegmentAgent, ID: "agent2"},
|
|
},
|
|
},
|
|
}
|
|
ctx = context.WithValue(ctx, globalResumeInfoKey{}, rInfo)
|
|
|
|
points, err := GetNextResumptionPoints(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, points, 2)
|
|
assert.True(t, points["agent1"])
|
|
assert.True(t, points["agent2"])
|
|
})
|
|
}
|
|
|
|
func TestBatchResumeWithData(t *testing.T) {
|
|
// Test Case 1: New Resume Data
|
|
t.Run("NewResumeData", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
resumeData := map[string]any{
|
|
"id1": "data1",
|
|
"id2": "data2",
|
|
}
|
|
|
|
newCtx := BatchResumeWithData(ctx, resumeData)
|
|
|
|
// Verify the data was set correctly
|
|
rInfo, ok := newCtx.Value(globalResumeInfoKey{}).(*globalResumeInfo)
|
|
assert.True(t, ok)
|
|
assert.NotNil(t, rInfo)
|
|
assert.Equal(t, "data1", rInfo.id2ResumeData["id1"])
|
|
assert.Equal(t, "data2", rInfo.id2ResumeData["id2"])
|
|
})
|
|
|
|
// Test Case 2: Merge with Existing Resume Data
|
|
t.Run("MergeWithExisting", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// First call with initial data
|
|
initialData := map[string]any{
|
|
"id1": "initial",
|
|
}
|
|
ctx = BatchResumeWithData(ctx, initialData)
|
|
|
|
// Second call with additional data
|
|
additionalData := map[string]any{
|
|
"id2": "additional",
|
|
}
|
|
newCtx := BatchResumeWithData(ctx, additionalData)
|
|
|
|
// Verify both data sets are present
|
|
rInfo, ok := newCtx.Value(globalResumeInfoKey{}).(*globalResumeInfo)
|
|
assert.True(t, ok)
|
|
assert.NotNil(t, rInfo)
|
|
assert.Equal(t, "initial", rInfo.id2ResumeData["id1"])
|
|
assert.Equal(t, "additional", rInfo.id2ResumeData["id2"])
|
|
})
|
|
|
|
// Test Case 3: Empty Resume Data
|
|
t.Run("EmptyResumeData", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
newCtx := BatchResumeWithData(ctx, map[string]any{})
|
|
|
|
rInfo, ok := newCtx.Value(globalResumeInfoKey{}).(*globalResumeInfo)
|
|
assert.True(t, ok)
|
|
assert.NotNil(t, rInfo)
|
|
assert.Empty(t, rInfo.id2ResumeData)
|
|
})
|
|
}
|
|
|
|
func TestGetInterruptState(t *testing.T) {
|
|
// Test Case 1: No Interrupt State
|
|
t.Run("NoInterruptState", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
wasInterrupted, hasState, state := GetInterruptState[string](ctx)
|
|
assert.False(t, wasInterrupted)
|
|
assert.False(t, hasState)
|
|
assert.Equal(t, "", state)
|
|
})
|
|
|
|
// Test Case 2: With Interrupt State
|
|
t.Run("WithInterruptState", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with interrupt state
|
|
runCtx := &addrCtx{
|
|
interruptState: &InterruptState{
|
|
State: "test state",
|
|
},
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
wasInterrupted, hasState, state := GetInterruptState[string](ctx)
|
|
assert.True(t, wasInterrupted)
|
|
assert.True(t, hasState)
|
|
assert.Equal(t, "test state", state)
|
|
})
|
|
|
|
// Test Case 3: Wrong Type for Interrupt State
|
|
t.Run("WrongType", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with interrupt state of wrong type
|
|
runCtx := &addrCtx{
|
|
interruptState: &InterruptState{
|
|
State: 123, // int instead of string
|
|
},
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
wasInterrupted, hasState, state := GetInterruptState[string](ctx)
|
|
assert.True(t, wasInterrupted)
|
|
assert.False(t, hasState) // Should be false due to type mismatch
|
|
assert.Equal(t, "", state)
|
|
})
|
|
|
|
// Test Case 4: Nil Interrupt State
|
|
t.Run("NilInterruptState", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with nil interrupt state
|
|
runCtx := &addrCtx{
|
|
interruptState: nil,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
wasInterrupted, hasState, state := GetInterruptState[string](ctx)
|
|
assert.False(t, wasInterrupted) // Should be false because interruptState is nil
|
|
assert.False(t, hasState) // Should be false because state is nil
|
|
assert.Equal(t, "", state)
|
|
})
|
|
}
|
|
|
|
func TestGetResumeContext(t *testing.T) {
|
|
// Test Case 1: Not Resume Target
|
|
t.Run("NotResumeTarget", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
isResumeTarget, hasData, data := GetResumeContext[string](ctx)
|
|
assert.False(t, isResumeTarget)
|
|
assert.False(t, hasData)
|
|
assert.Equal(t, "", data)
|
|
})
|
|
|
|
// Test Case 2: Resume Target with Data
|
|
t.Run("ResumeTargetWithData", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context as resume target with data
|
|
runCtx := &addrCtx{
|
|
isResumeTarget: true,
|
|
resumeData: "resume data",
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
isResumeTarget, hasData, data := GetResumeContext[string](ctx)
|
|
assert.True(t, isResumeTarget)
|
|
assert.True(t, hasData)
|
|
assert.Equal(t, "resume data", data)
|
|
})
|
|
|
|
// Test Case 3: Resume Target without Data
|
|
t.Run("ResumeTargetWithoutData", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context as resume target without data
|
|
runCtx := &addrCtx{
|
|
isResumeTarget: true,
|
|
resumeData: nil,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
isResumeTarget, hasData, data := GetResumeContext[string](ctx)
|
|
assert.True(t, isResumeTarget)
|
|
assert.False(t, hasData)
|
|
assert.Equal(t, "", data)
|
|
})
|
|
|
|
// Test Case 4: Wrong Type for Resume Data
|
|
t.Run("WrongType", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with resume data of wrong type
|
|
runCtx := &addrCtx{
|
|
isResumeTarget: true,
|
|
resumeData: 123, // int instead of string
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
isResumeTarget, hasData, data := GetResumeContext[string](ctx)
|
|
assert.True(t, isResumeTarget)
|
|
assert.False(t, hasData) // Should be false due to type mismatch
|
|
assert.Equal(t, "", data)
|
|
})
|
|
}
|
|
|
|
func TestWithLayerPayload(t *testing.T) {
|
|
// Test Case 1: Basic Usage
|
|
t.Run("BasicUsage", func(t *testing.T) {
|
|
config := &InterruptConfig{}
|
|
opt := WithLayerPayload("test payload")
|
|
opt(config)
|
|
assert.Equal(t, "test payload", config.LayerPayload)
|
|
})
|
|
|
|
// Test Case 2: Nil Payload
|
|
t.Run("NilPayload", func(t *testing.T) {
|
|
config := &InterruptConfig{LayerPayload: "existing"}
|
|
opt := WithLayerPayload(nil)
|
|
opt(config)
|
|
assert.Nil(t, config.LayerPayload)
|
|
})
|
|
|
|
// Test Case 3: Complex Payload
|
|
t.Run("ComplexPayload", func(t *testing.T) {
|
|
config := &InterruptConfig{}
|
|
payload := map[string]any{
|
|
"key1": "value1",
|
|
"key2": 123,
|
|
}
|
|
opt := WithLayerPayload(payload)
|
|
opt(config)
|
|
assert.Equal(t, payload, config.LayerPayload)
|
|
})
|
|
}
|
|
|
|
func TestInterruptFunction(t *testing.T) {
|
|
// Test Case 1: Simple Interrupt without SubContexts
|
|
t.Run("SimpleInterrupt", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with a mock address
|
|
expectedAddr := Address{{Type: AddressSegmentAgent, ID: "test-agent"}}
|
|
runCtx := &addrCtx{
|
|
addr: expectedAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
info := "test info"
|
|
state := "test state"
|
|
|
|
signal, err := Interrupt(ctx, info, state, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, signal)
|
|
assert.NotEmpty(t, signal.ID)
|
|
assert.Equal(t, info, signal.Info)
|
|
assert.Equal(t, state, signal.State)
|
|
assert.True(t, signal.IsRootCause)
|
|
assert.Equal(t, expectedAddr, signal.Address)
|
|
})
|
|
|
|
// Test Case 2: Interrupt with SubContexts
|
|
t.Run("InterruptWithSubContexts", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with a mock address
|
|
expectedAddr := Address{{Type: AddressSegmentAgent, ID: "parent-agent"}}
|
|
runCtx := &addrCtx{
|
|
addr: expectedAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
// Create sub contexts
|
|
subContexts := []*InterruptSignal{
|
|
{
|
|
ID: "child1",
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "child1"}},
|
|
},
|
|
{
|
|
ID: "child2",
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "child2"}},
|
|
},
|
|
}
|
|
|
|
info := "parent info"
|
|
state := "parent state"
|
|
|
|
signal, err := Interrupt(ctx, info, state, subContexts)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, signal)
|
|
assert.NotEmpty(t, signal.ID)
|
|
assert.Equal(t, info, signal.Info)
|
|
assert.Equal(t, state, signal.State)
|
|
assert.False(t, signal.IsRootCause) // Should be false when there are sub contexts
|
|
assert.Len(t, signal.Subs, 2)
|
|
assert.Equal(t, "child1", signal.Subs[0].ID)
|
|
assert.Equal(t, "child2", signal.Subs[1].ID)
|
|
})
|
|
|
|
// Test Case 3: Interrupt with Options
|
|
t.Run("InterruptWithOptions", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with a mock address
|
|
expectedAddr := Address{{Type: AddressSegmentAgent, ID: "test-agent"}}
|
|
runCtx := &addrCtx{
|
|
addr: expectedAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
info := "test info"
|
|
state := "test state"
|
|
layerPayload := "layer payload"
|
|
|
|
signal, err := Interrupt(ctx, info, state, nil, WithLayerPayload(layerPayload))
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, signal)
|
|
assert.Equal(t, layerPayload, signal.LayerSpecificPayload)
|
|
})
|
|
|
|
// Test Case 4: Empty SubContexts
|
|
t.Run("EmptySubContexts", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a context with a mock address
|
|
expectedAddr := Address{{Type: AddressSegmentAgent, ID: "test-agent"}}
|
|
runCtx := &addrCtx{
|
|
addr: expectedAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
info := "test info"
|
|
state := "test state"
|
|
|
|
signal, err := Interrupt(ctx, info, state, []*InterruptSignal{})
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, signal)
|
|
assert.True(t, signal.IsRootCause) // Should be true when sub contexts is empty
|
|
assert.Empty(t, signal.Subs)
|
|
})
|
|
}
|
|
|
|
func TestAddressMethods(t *testing.T) {
|
|
// Test Case 1: Address.String()
|
|
t.Run("AddressString", func(t *testing.T) {
|
|
addr := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
{Type: AddressSegmentNode, ID: "node1", SubID: "sub1"},
|
|
}
|
|
|
|
result := addr.String()
|
|
expected := "agent:agent1;tool:tool1;node:node1:sub1"
|
|
assert.Equal(t, expected, result)
|
|
})
|
|
|
|
// Test Case 2: Address.String() with empty address
|
|
t.Run("EmptyAddressString", func(t *testing.T) {
|
|
var addr Address
|
|
result := addr.String()
|
|
assert.Equal(t, "", result)
|
|
})
|
|
|
|
// Test Case 3: Address.Equals() with equal addresses
|
|
t.Run("AddressEquals", func(t *testing.T) {
|
|
addr1 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
}
|
|
addr2 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
}
|
|
|
|
assert.True(t, addr1.Equals(addr2))
|
|
})
|
|
|
|
// Test Case 4: Address.Equals() with different addresses
|
|
t.Run("AddressNotEquals", func(t *testing.T) {
|
|
addr1 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
}
|
|
addr2 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool2"},
|
|
}
|
|
|
|
assert.False(t, addr1.Equals(addr2))
|
|
})
|
|
|
|
// Test Case 5: Address.Equals() with different lengths
|
|
t.Run("AddressDifferentLengths", func(t *testing.T) {
|
|
addr1 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
{Type: AddressSegmentTool, ID: "tool1"},
|
|
}
|
|
addr2 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1"},
|
|
}
|
|
|
|
assert.False(t, addr1.Equals(addr2))
|
|
})
|
|
|
|
// Test Case 6: Address.Equals() with SubID differences
|
|
t.Run("AddressSubIDDifference", func(t *testing.T) {
|
|
addr1 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1", SubID: "sub1"},
|
|
}
|
|
addr2 := Address{
|
|
{Type: AddressSegmentAgent, ID: "agent1", SubID: "sub2"},
|
|
}
|
|
|
|
assert.False(t, addr1.Equals(addr2))
|
|
})
|
|
}
|
|
|
|
func TestAppendAddressSegment(t *testing.T) {
|
|
// Test Case 1: Append to empty address
|
|
t.Run("AppendToEmpty", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
newCtx := AppendAddressSegment(ctx, AddressSegmentAgent, "agent1", "")
|
|
|
|
addr := GetCurrentAddress(newCtx)
|
|
assert.Len(t, addr, 1)
|
|
assert.Equal(t, AddressSegmentAgent, addr[0].Type)
|
|
assert.Equal(t, "agent1", addr[0].ID)
|
|
assert.Equal(t, "", addr[0].SubID)
|
|
})
|
|
|
|
// Test Case 2: Append to existing address
|
|
t.Run("AppendToExisting", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// First append
|
|
ctx = AppendAddressSegment(ctx, AddressSegmentAgent, "agent1", "")
|
|
|
|
// Second append
|
|
newCtx := AppendAddressSegment(ctx, AddressSegmentTool, "tool1", "call1")
|
|
|
|
addr := GetCurrentAddress(newCtx)
|
|
assert.Len(t, addr, 2)
|
|
assert.Equal(t, AddressSegmentAgent, addr[0].Type)
|
|
assert.Equal(t, "agent1", addr[0].ID)
|
|
assert.Equal(t, AddressSegmentTool, addr[1].Type)
|
|
assert.Equal(t, "tool1", addr[1].ID)
|
|
assert.Equal(t, "call1", addr[1].SubID)
|
|
})
|
|
|
|
// Test Case 3: Append with SubID
|
|
t.Run("AppendWithSubID", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
newCtx := AppendAddressSegment(ctx, AddressSegmentTool, "tool1", "call123")
|
|
|
|
addr := GetCurrentAddress(newCtx)
|
|
assert.Len(t, addr, 1)
|
|
assert.Equal(t, AddressSegmentTool, addr[0].Type)
|
|
assert.Equal(t, "tool1", addr[0].ID)
|
|
assert.Equal(t, "call123", addr[0].SubID)
|
|
})
|
|
}
|
|
|
|
func TestPopulateInterruptState(t *testing.T) {
|
|
// Test Case 1: Populate with matching address
|
|
t.Run("PopulateMatchingAddress", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Set up current address
|
|
currentAddr := Address{{Type: AddressSegmentAgent, ID: "agent1"}}
|
|
runCtx := &addrCtx{
|
|
addr: currentAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
// Set up interrupt state data
|
|
id2Addr := map[string]Address{
|
|
"interrupt1": currentAddr,
|
|
}
|
|
id2State := map[string]InterruptState{
|
|
"interrupt1": {State: "test state"},
|
|
}
|
|
|
|
newCtx := PopulateInterruptState(ctx, id2Addr, id2State)
|
|
|
|
// Verify the state was populated
|
|
wasInterrupted, hasState, state := GetInterruptState[string](newCtx)
|
|
assert.True(t, wasInterrupted)
|
|
assert.True(t, hasState)
|
|
assert.Equal(t, "test state", state)
|
|
})
|
|
|
|
// Test Case 2: Populate with non-matching address
|
|
t.Run("PopulateNonMatchingAddress", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Set up current address
|
|
currentAddr := Address{{Type: AddressSegmentAgent, ID: "agent1"}}
|
|
runCtx := &addrCtx{
|
|
addr: currentAddr,
|
|
}
|
|
ctx = context.WithValue(ctx, addrCtxKey{}, runCtx)
|
|
|
|
// Set up interrupt state data with different address
|
|
id2Addr := map[string]Address{
|
|
"interrupt1": {{Type: AddressSegmentAgent, ID: "agent2"}},
|
|
}
|
|
id2State := map[string]InterruptState{
|
|
"interrupt1": {State: "test state"},
|
|
}
|
|
|
|
newCtx := PopulateInterruptState(ctx, id2Addr, id2State)
|
|
|
|
// Verify the state was NOT populated (no matching address)
|
|
wasInterrupted, hasState, state := GetInterruptState[string](newCtx)
|
|
assert.False(t, wasInterrupted)
|
|
assert.False(t, hasState)
|
|
assert.Equal(t, "", state)
|
|
})
|
|
|
|
// Test Case 3: Populate with empty data
|
|
t.Run("PopulateEmptyData", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
newCtx := PopulateInterruptState(ctx, map[string]Address{}, map[string]InterruptState{})
|
|
|
|
// Verify no state was populated
|
|
wasInterrupted, hasState, state := GetInterruptState[string](newCtx)
|
|
assert.False(t, wasInterrupted)
|
|
assert.False(t, hasState)
|
|
assert.Equal(t, "", state)
|
|
})
|
|
}
|
|
|
|
func TestStringMethods(t *testing.T) {
|
|
// Test Case 1: InterruptSignal.Error()
|
|
t.Run("InterruptSignalError", func(t *testing.T) {
|
|
signal := &InterruptSignal{
|
|
ID: "test-id",
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
InterruptInfo: InterruptInfo{
|
|
Info: "test info",
|
|
},
|
|
InterruptState: InterruptState{
|
|
State: "test state",
|
|
LayerSpecificPayload: "test payload",
|
|
},
|
|
Subs: []*InterruptSignal{
|
|
{ID: "sub1"},
|
|
},
|
|
}
|
|
|
|
errorStr := signal.Error()
|
|
expectedContains := []string{
|
|
"interrupt signal:",
|
|
"ID=test-id",
|
|
"Addr=agent:agent1",
|
|
"Info=interrupt info: Info=test info, IsRootCause=false",
|
|
"State=interrupt state: State=test state, LayerSpecificPayload=test payload",
|
|
"SubsLen=1",
|
|
}
|
|
|
|
for _, expected := range expectedContains {
|
|
assert.Contains(t, errorStr, expected)
|
|
}
|
|
})
|
|
|
|
// Test Case 2: InterruptState.String()
|
|
t.Run("InterruptStateString", func(t *testing.T) {
|
|
state := &InterruptState{
|
|
State: "test state",
|
|
LayerSpecificPayload: "test payload",
|
|
}
|
|
|
|
result := state.String()
|
|
expected := "interrupt state: State=test state, LayerSpecificPayload=test payload"
|
|
assert.Equal(t, expected, result)
|
|
})
|
|
|
|
// Test Case 3: InterruptState.String() with nil
|
|
t.Run("InterruptStateStringNil", func(t *testing.T) {
|
|
var state *InterruptState
|
|
result := state.String()
|
|
assert.Equal(t, "", result)
|
|
})
|
|
|
|
// Test Case 4: InterruptInfo.String()
|
|
t.Run("InterruptInfoString", func(t *testing.T) {
|
|
info := &InterruptInfo{
|
|
Info: "test info",
|
|
IsRootCause: true,
|
|
}
|
|
|
|
result := info.String()
|
|
expected := "interrupt info: Info=test info, IsRootCause=true"
|
|
assert.Equal(t, expected, result)
|
|
})
|
|
|
|
// Test Case 5: InterruptInfo.String() with nil
|
|
t.Run("InterruptInfoStringNil", func(t *testing.T) {
|
|
var info *InterruptInfo
|
|
result := info.String()
|
|
assert.Equal(t, "", result)
|
|
})
|
|
}
|
|
|
|
func TestInterruptCtxEqualsWithoutID(t *testing.T) {
|
|
// Test Case 1: Equal contexts
|
|
t.Run("EqualContexts", func(t *testing.T) {
|
|
ctx1 := &InterruptCtx{
|
|
ID: "id1",
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Info: "info1",
|
|
IsRootCause: true,
|
|
}
|
|
ctx2 := &InterruptCtx{
|
|
ID: "id2", // Different ID should be ignored
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Info: "info1",
|
|
IsRootCause: true,
|
|
}
|
|
|
|
assert.True(t, ctx1.EqualsWithoutID(ctx2))
|
|
})
|
|
|
|
// Test Case 2: Different addresses
|
|
t.Run("DifferentAddresses", func(t *testing.T) {
|
|
ctx1 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
}
|
|
ctx2 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent2"}},
|
|
}
|
|
|
|
assert.False(t, ctx1.EqualsWithoutID(ctx2))
|
|
})
|
|
|
|
// Test Case 3: Different root cause flags
|
|
t.Run("DifferentRootCause", func(t *testing.T) {
|
|
ctx1 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
IsRootCause: true,
|
|
}
|
|
ctx2 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
IsRootCause: false,
|
|
}
|
|
|
|
assert.False(t, ctx1.EqualsWithoutID(ctx2))
|
|
})
|
|
|
|
// Test Case 4: Different info
|
|
t.Run("DifferentInfo", func(t *testing.T) {
|
|
ctx1 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Info: "info1",
|
|
}
|
|
ctx2 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Info: "info2",
|
|
}
|
|
|
|
assert.False(t, ctx1.EqualsWithoutID(ctx2))
|
|
})
|
|
|
|
// Test Case 5: Nil contexts
|
|
t.Run("NilContexts", func(t *testing.T) {
|
|
var ctx1 *InterruptCtx
|
|
var ctx2 *InterruptCtx
|
|
|
|
assert.True(t, ctx1.EqualsWithoutID(ctx2))
|
|
|
|
ctx3 := &InterruptCtx{}
|
|
assert.False(t, ctx1.EqualsWithoutID(ctx3))
|
|
assert.False(t, ctx3.EqualsWithoutID(ctx1))
|
|
})
|
|
|
|
// Test Case 6: With parent contexts
|
|
t.Run("WithParentContexts", func(t *testing.T) {
|
|
parent1 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "parent"}},
|
|
}
|
|
parent2 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "parent"}},
|
|
}
|
|
|
|
ctx1 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Parent: parent1,
|
|
}
|
|
ctx2 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Parent: parent2,
|
|
}
|
|
|
|
assert.True(t, ctx1.EqualsWithoutID(ctx2))
|
|
})
|
|
|
|
// Test Case 7: Different parent contexts
|
|
t.Run("DifferentParentContexts", func(t *testing.T) {
|
|
parent1 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "parent1"}},
|
|
}
|
|
parent2 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "parent2"}},
|
|
}
|
|
|
|
ctx1 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Parent: parent1,
|
|
}
|
|
ctx2 := &InterruptCtx{
|
|
Address: Address{{Type: AddressSegmentAgent, ID: "agent1"}},
|
|
Parent: parent2,
|
|
}
|
|
|
|
assert.False(t, ctx1.EqualsWithoutID(ctx2))
|
|
})
|
|
}
|