Complete BZZZ functionality port to CHORUS
🎭 CHORUS now contains full BZZZ functionality adapted for containers Core systems ported: - P2P networking (libp2p with DHT and PubSub) - Task coordination (COOEE protocol) - HMMM collaborative reasoning - SHHH encryption and security - SLURP admin election system - UCXL content addressing - UCXI server integration - Hypercore logging system - Health monitoring and graceful shutdown - License validation with KACHING Container adaptations: - Environment variable configuration (no YAML files) - Container-optimized logging to stdout/stderr - Auto-generated agent IDs for container deployments - Docker-first architecture All proven BZZZ P2P protocols, AI integration, and collaboration features are now available in containerized form. Next: Build and test container deployment. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
369
pkg/ucxl/address.go
Normal file
369
pkg/ucxl/address.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package ucxl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Address represents a parsed UCXL address
|
||||
// Format: ucxl://agent:role@project:task/temporal_segment/path
|
||||
type Address struct {
|
||||
// Core components
|
||||
Agent string `json:"agent"`
|
||||
Role string `json:"role"`
|
||||
Project string `json:"project"`
|
||||
Task string `json:"task"`
|
||||
|
||||
// Temporal component
|
||||
TemporalSegment TemporalSegment `json:"temporal_segment"`
|
||||
|
||||
// Path component
|
||||
Path string `json:"path"`
|
||||
|
||||
// Original raw address for reference
|
||||
Raw string `json:"raw"`
|
||||
}
|
||||
|
||||
// TemporalSegment represents temporal navigation information
|
||||
type TemporalSegment struct {
|
||||
Type TemporalType `json:"type"`
|
||||
Direction Direction `json:"direction,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
}
|
||||
|
||||
// TemporalType defines the type of temporal navigation
|
||||
type TemporalType string
|
||||
|
||||
const (
|
||||
TemporalLatest TemporalType = "latest" // *^
|
||||
TemporalAny TemporalType = "any" // *~
|
||||
TemporalSpecific TemporalType = "specific" // *~N
|
||||
TemporalRelative TemporalType = "relative" // ~~N, ^^N
|
||||
)
|
||||
|
||||
// Direction defines temporal navigation direction
|
||||
type Direction string
|
||||
|
||||
const (
|
||||
DirectionBackward Direction = "backward" // ~~
|
||||
DirectionForward Direction = "forward" // ^^
|
||||
)
|
||||
|
||||
// ValidationError represents an address validation error
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
Raw string
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("UCXL address validation error in %s: %s (address: %s)", e.Field, e.Message, e.Raw)
|
||||
}
|
||||
|
||||
// Regular expressions for validation
|
||||
var (
|
||||
// Component validation patterns
|
||||
componentPattern = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$|^any$`)
|
||||
pathPattern = regexp.MustCompile(`^[a-zA-Z0-9_\-/\.]*$`)
|
||||
|
||||
// Temporal segment patterns
|
||||
temporalLatestPattern = regexp.MustCompile(`^\*\^$`) // *^
|
||||
temporalAnyPattern = regexp.MustCompile(`^\*~$`) // *~
|
||||
temporalSpecificPattern = regexp.MustCompile(`^\*~(\d+)$`) // *~N
|
||||
temporalBackwardPattern = regexp.MustCompile(`^~~(\d+)$`) // ~~N
|
||||
temporalForwardPattern = regexp.MustCompile(`^\^\^(\d+)$`) // ^^N
|
||||
|
||||
// Full address pattern for initial validation
|
||||
ucxlAddressPattern = regexp.MustCompile(`^ucxl://([^:]+):([^@]+)@([^:]+):([^/]+)/([^/]+)/?(.*)$`)
|
||||
)
|
||||
|
||||
// Parse parses a UCXL address string into an Address struct
|
||||
func Parse(address string) (*Address, error) {
|
||||
if address == "" {
|
||||
return nil, &ValidationError{
|
||||
Field: "address",
|
||||
Message: "address cannot be empty",
|
||||
Raw: address,
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the address (trim whitespace, convert to lowercase for scheme)
|
||||
normalized := strings.TrimSpace(address)
|
||||
if !strings.HasPrefix(strings.ToLower(normalized), "ucxl://") {
|
||||
return nil, &ValidationError{
|
||||
Field: "scheme",
|
||||
Message: "address must start with 'ucxl://'",
|
||||
Raw: address,
|
||||
}
|
||||
}
|
||||
|
||||
// Check scheme manually since our format doesn't follow standard URL format
|
||||
if !strings.HasPrefix(strings.ToLower(normalized), "ucxl://") {
|
||||
return nil, &ValidationError{
|
||||
Field: "scheme",
|
||||
Message: "scheme must be 'ucxl'",
|
||||
Raw: address,
|
||||
}
|
||||
}
|
||||
|
||||
// Use regex for detailed component extraction
|
||||
// Convert to lowercase for scheme but keep original for case-sensitive parts
|
||||
normalizedForPattern := strings.ToLower(normalized[:7]) + normalized[7:] // normalize "ucxl://" part
|
||||
matches := ucxlAddressPattern.FindStringSubmatch(normalizedForPattern)
|
||||
if matches == nil || len(matches) != 7 {
|
||||
return nil, &ValidationError{
|
||||
Field: "format",
|
||||
Message: "address format must be 'ucxl://agent:role@project:task/temporal_segment/path'",
|
||||
Raw: address,
|
||||
}
|
||||
}
|
||||
|
||||
addr := &Address{
|
||||
Agent: normalizeComponent(matches[1]),
|
||||
Role: normalizeComponent(matches[2]),
|
||||
Project: normalizeComponent(matches[3]),
|
||||
Task: normalizeComponent(matches[4]),
|
||||
Path: matches[6], // Path can be empty
|
||||
Raw: address,
|
||||
}
|
||||
|
||||
// Parse temporal segment
|
||||
temporalSegment, err := parseTemporalSegment(matches[5])
|
||||
if err != nil {
|
||||
return nil, &ValidationError{
|
||||
Field: "temporal_segment",
|
||||
Message: err.Error(),
|
||||
Raw: address,
|
||||
}
|
||||
}
|
||||
addr.TemporalSegment = *temporalSegment
|
||||
|
||||
// Validate all components
|
||||
if err := addr.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// parseTemporalSegment parses the temporal segment component
|
||||
func parseTemporalSegment(segment string) (*TemporalSegment, error) {
|
||||
if segment == "" {
|
||||
return nil, fmt.Errorf("temporal segment cannot be empty")
|
||||
}
|
||||
|
||||
// Check for latest (*^)
|
||||
if temporalLatestPattern.MatchString(segment) {
|
||||
return &TemporalSegment{Type: TemporalLatest}, nil
|
||||
}
|
||||
|
||||
// Check for any (*~)
|
||||
if temporalAnyPattern.MatchString(segment) {
|
||||
return &TemporalSegment{Type: TemporalAny}, nil
|
||||
}
|
||||
|
||||
// Check for specific version (*~N)
|
||||
if matches := temporalSpecificPattern.FindStringSubmatch(segment); matches != nil {
|
||||
count, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid version number in specific temporal segment: %s", matches[1])
|
||||
}
|
||||
if count < 0 {
|
||||
return nil, fmt.Errorf("version number cannot be negative: %d", count)
|
||||
}
|
||||
return &TemporalSegment{
|
||||
Type: TemporalSpecific,
|
||||
Count: count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check for backward navigation (~~N)
|
||||
if matches := temporalBackwardPattern.FindStringSubmatch(segment); matches != nil {
|
||||
count, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid count in backward temporal segment: %s", matches[1])
|
||||
}
|
||||
if count < 0 {
|
||||
return nil, fmt.Errorf("backward count cannot be negative: %d", count)
|
||||
}
|
||||
return &TemporalSegment{
|
||||
Type: TemporalRelative,
|
||||
Direction: DirectionBackward,
|
||||
Count: count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check for forward navigation (^^N)
|
||||
if matches := temporalForwardPattern.FindStringSubmatch(segment); matches != nil {
|
||||
count, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid count in forward temporal segment: %s", matches[1])
|
||||
}
|
||||
if count < 0 {
|
||||
return nil, fmt.Errorf("forward count cannot be negative: %d", count)
|
||||
}
|
||||
return &TemporalSegment{
|
||||
Type: TemporalRelative,
|
||||
Direction: DirectionForward,
|
||||
Count: count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid temporal segment format: %s", segment)
|
||||
}
|
||||
|
||||
// normalizeComponent normalizes address components (case-insensitive)
|
||||
func normalizeComponent(component string) string {
|
||||
return strings.ToLower(strings.TrimSpace(component))
|
||||
}
|
||||
|
||||
// Validate validates the Address components according to BNF grammar rules
|
||||
func (a *Address) Validate() error {
|
||||
// Validate agent component
|
||||
if err := validateComponent("agent", a.Agent); err != nil {
|
||||
return &ValidationError{
|
||||
Field: "agent",
|
||||
Message: err.Error(),
|
||||
Raw: a.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate role component
|
||||
if err := validateComponent("role", a.Role); err != nil {
|
||||
return &ValidationError{
|
||||
Field: "role",
|
||||
Message: err.Error(),
|
||||
Raw: a.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate project component
|
||||
if err := validateComponent("project", a.Project); err != nil {
|
||||
return &ValidationError{
|
||||
Field: "project",
|
||||
Message: err.Error(),
|
||||
Raw: a.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate task component
|
||||
if err := validateComponent("task", a.Task); err != nil {
|
||||
return &ValidationError{
|
||||
Field: "task",
|
||||
Message: err.Error(),
|
||||
Raw: a.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate path component (can be empty)
|
||||
if a.Path != "" && !pathPattern.MatchString(a.Path) {
|
||||
return &ValidationError{
|
||||
Field: "path",
|
||||
Message: "path can only contain alphanumeric characters, underscores, hyphens, forward slashes, and dots",
|
||||
Raw: a.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateComponent validates individual address components
|
||||
func validateComponent(name, component string) error {
|
||||
if component == "" {
|
||||
return fmt.Errorf("%s cannot be empty", name)
|
||||
}
|
||||
|
||||
if !componentPattern.MatchString(component) {
|
||||
return fmt.Errorf("%s can only contain alphanumeric characters, underscores, hyphens, or be 'any'", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the canonical string representation of the address
|
||||
func (a *Address) String() string {
|
||||
temporalStr := a.TemporalSegment.String()
|
||||
if a.Path != "" {
|
||||
return fmt.Sprintf("ucxl://%s:%s@%s:%s/%s/%s", a.Agent, a.Role, a.Project, a.Task, temporalStr, a.Path)
|
||||
}
|
||||
return fmt.Sprintf("ucxl://%s:%s@%s:%s/%s", a.Agent, a.Role, a.Project, a.Task, temporalStr)
|
||||
}
|
||||
|
||||
// String returns the string representation of the temporal segment
|
||||
func (ts *TemporalSegment) String() string {
|
||||
switch ts.Type {
|
||||
case TemporalLatest:
|
||||
return "*^"
|
||||
case TemporalAny:
|
||||
return "*~"
|
||||
case TemporalSpecific:
|
||||
return fmt.Sprintf("*~%d", ts.Count)
|
||||
case TemporalRelative:
|
||||
if ts.Direction == DirectionBackward {
|
||||
return fmt.Sprintf("~~%d", ts.Count)
|
||||
}
|
||||
return fmt.Sprintf("^^%d", ts.Count)
|
||||
default:
|
||||
return "*^" // Default to latest
|
||||
}
|
||||
}
|
||||
|
||||
// IsWildcard returns true if the address uses wildcard patterns
|
||||
func (a *Address) IsWildcard() bool {
|
||||
return a.Agent == "any" || a.Role == "any" || a.Project == "any" || a.Task == "any"
|
||||
}
|
||||
|
||||
// Matches returns true if this address matches the pattern address
|
||||
// Supports wildcard matching where "any" matches any value
|
||||
func (a *Address) Matches(pattern *Address) bool {
|
||||
if pattern == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check each component for wildcard or exact match
|
||||
if pattern.Agent != "any" && a.Agent != pattern.Agent {
|
||||
return false
|
||||
}
|
||||
if pattern.Role != "any" && a.Role != pattern.Role {
|
||||
return false
|
||||
}
|
||||
if pattern.Project != "any" && a.Project != pattern.Project {
|
||||
return false
|
||||
}
|
||||
if pattern.Task != "any" && a.Task != pattern.Task {
|
||||
return false
|
||||
}
|
||||
|
||||
// Path matching (if pattern has path, address must match or be subset)
|
||||
if pattern.Path != "" {
|
||||
if a.Path == "" {
|
||||
return false
|
||||
}
|
||||
// Simple prefix matching for paths
|
||||
if !strings.HasPrefix(a.Path, pattern.Path) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of the address
|
||||
func (a *Address) Clone() *Address {
|
||||
return &Address{
|
||||
Agent: a.Agent,
|
||||
Role: a.Role,
|
||||
Project: a.Project,
|
||||
Task: a.Task,
|
||||
TemporalSegment: a.TemporalSegment, // TemporalSegment is a value type, safe to copy
|
||||
Path: a.Path,
|
||||
Raw: a.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid performs comprehensive validation and returns true if the address is valid
|
||||
func (a *Address) IsValid() bool {
|
||||
return a.Validate() == nil
|
||||
}
|
||||
508
pkg/ucxl/address_test.go
Normal file
508
pkg/ucxl/address_test.go
Normal file
@@ -0,0 +1,508 @@
|
||||
package ucxl
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseValidAddresses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
expected *Address
|
||||
}{
|
||||
{
|
||||
name: "simple latest address",
|
||||
address: "ucxl://agent1:developer@project1:task1/*^",
|
||||
expected: &Address{
|
||||
Agent: "agent1",
|
||||
Role: "developer",
|
||||
Project: "project1",
|
||||
Task: "task1",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalLatest,
|
||||
},
|
||||
Path: "",
|
||||
Raw: "ucxl://agent1:developer@project1:task1/*^",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "address with path",
|
||||
address: "ucxl://agent2:tester@project2:task2/*~/path/to/file.txt",
|
||||
expected: &Address{
|
||||
Agent: "agent2",
|
||||
Role: "tester",
|
||||
Project: "project2",
|
||||
Task: "task2",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalAny,
|
||||
},
|
||||
Path: "path/to/file.txt",
|
||||
Raw: "ucxl://agent2:tester@project2:task2/*~/path/to/file.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "specific version address",
|
||||
address: "ucxl://any:any@project3:task3/*~5/config.json",
|
||||
expected: &Address{
|
||||
Agent: "any",
|
||||
Role: "any",
|
||||
Project: "project3",
|
||||
Task: "task3",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalSpecific,
|
||||
Count: 5,
|
||||
},
|
||||
Path: "config.json",
|
||||
Raw: "ucxl://any:any@project3:task3/*~5/config.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backward navigation address",
|
||||
address: "ucxl://bot:admin@system:backup/~~3",
|
||||
expected: &Address{
|
||||
Agent: "bot",
|
||||
Role: "admin",
|
||||
Project: "system",
|
||||
Task: "backup",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalRelative,
|
||||
Direction: DirectionBackward,
|
||||
Count: 3,
|
||||
},
|
||||
Path: "",
|
||||
Raw: "ucxl://bot:admin@system:backup/~~3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "forward navigation address",
|
||||
address: "ucxl://ai:researcher@analysis:data/^^2/results",
|
||||
expected: &Address{
|
||||
Agent: "ai",
|
||||
Role: "researcher",
|
||||
Project: "analysis",
|
||||
Task: "data",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalRelative,
|
||||
Direction: DirectionForward,
|
||||
Count: 2,
|
||||
},
|
||||
Path: "results",
|
||||
Raw: "ucxl://ai:researcher@analysis:data/^^2/results",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "case normalization",
|
||||
address: "UCXL://AGENT1:DEVELOPER@PROJECT1:TASK1/*^",
|
||||
expected: &Address{
|
||||
Agent: "agent1",
|
||||
Role: "developer",
|
||||
Project: "project1",
|
||||
Task: "task1",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalLatest,
|
||||
},
|
||||
Path: "",
|
||||
Raw: "UCXL://AGENT1:DEVELOPER@PROJECT1:TASK1/*^",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Parse(tt.address)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("Parse() = %+v, want %+v", result, tt.expected)
|
||||
}
|
||||
|
||||
// Test that the address is valid
|
||||
if !result.IsValid() {
|
||||
t.Errorf("Parsed address should be valid but IsValid() returned false")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInvalidAddresses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty address",
|
||||
address: "",
|
||||
wantErr: "address cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "wrong scheme",
|
||||
address: "http://agent:role@project:task/*^",
|
||||
wantErr: "scheme must be 'ucxl'",
|
||||
},
|
||||
{
|
||||
name: "missing scheme",
|
||||
address: "agent:role@project:task/*^",
|
||||
wantErr: "address must start with 'ucxl://'",
|
||||
},
|
||||
{
|
||||
name: "invalid format",
|
||||
address: "ucxl://invalid-format",
|
||||
wantErr: "address format must be",
|
||||
},
|
||||
{
|
||||
name: "empty agent",
|
||||
address: "ucxl://:role@project:task/*^",
|
||||
wantErr: "agent cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "empty role",
|
||||
address: "ucxl://agent:@project:task/*^",
|
||||
wantErr: "role cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "empty project",
|
||||
address: "ucxl://agent:role@:task/*^",
|
||||
wantErr: "project cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "empty task",
|
||||
address: "ucxl://agent:role@project:/*^",
|
||||
wantErr: "task cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "invalid temporal segment",
|
||||
address: "ucxl://agent:role@project:task/invalid",
|
||||
wantErr: "invalid temporal segment format",
|
||||
},
|
||||
{
|
||||
name: "negative version",
|
||||
address: "ucxl://agent:role@project:task/*~-1",
|
||||
wantErr: "version number cannot be negative",
|
||||
},
|
||||
{
|
||||
name: "negative backward count",
|
||||
address: "ucxl://agent:role@project:task/~~-5",
|
||||
wantErr: "backward count cannot be negative",
|
||||
},
|
||||
{
|
||||
name: "invalid characters in component",
|
||||
address: "ucxl://agent!:role@project:task/*^",
|
||||
wantErr: "agent can only contain alphanumeric",
|
||||
},
|
||||
{
|
||||
name: "invalid path characters",
|
||||
address: "ucxl://agent:role@project:task/*^/path with spaces",
|
||||
wantErr: "path can only contain alphanumeric",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Parse(tt.address)
|
||||
if err == nil {
|
||||
t.Fatalf("Parse() expected error containing '%s', got nil", tt.wantErr)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("Parse() should return nil on error, got %+v", result)
|
||||
}
|
||||
|
||||
if err.Error() == "" {
|
||||
t.Errorf("Error message should not be empty")
|
||||
}
|
||||
|
||||
// Check if error contains expected substring (case insensitive)
|
||||
// This allows for more flexible error message matching
|
||||
// In production tests, you might want exact matching
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address *Address
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple address without path",
|
||||
address: &Address{
|
||||
Agent: "agent1",
|
||||
Role: "developer",
|
||||
Project: "project1",
|
||||
Task: "task1",
|
||||
TemporalSegment: TemporalSegment{Type: TemporalLatest},
|
||||
},
|
||||
expected: "ucxl://agent1:developer@project1:task1/*^",
|
||||
},
|
||||
{
|
||||
name: "address with path",
|
||||
address: &Address{
|
||||
Agent: "agent2",
|
||||
Role: "tester",
|
||||
Project: "project2",
|
||||
Task: "task2",
|
||||
TemporalSegment: TemporalSegment{Type: TemporalAny},
|
||||
Path: "path/to/file.txt",
|
||||
},
|
||||
expected: "ucxl://agent2:tester@project2:task2/*~/path/to/file.txt",
|
||||
},
|
||||
{
|
||||
name: "specific version",
|
||||
address: &Address{
|
||||
Agent: "any",
|
||||
Role: "any",
|
||||
Project: "project3",
|
||||
Task: "task3",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalSpecific,
|
||||
Count: 10,
|
||||
},
|
||||
},
|
||||
expected: "ucxl://any:any@project3:task3/*~10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.address.String()
|
||||
if result != tt.expected {
|
||||
t.Errorf("String() = %s, want %s", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressMatches(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address *Address
|
||||
pattern *Address
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
address: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
pattern: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard agent match",
|
||||
address: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
pattern: &Address{
|
||||
Agent: "any", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard all match",
|
||||
address: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
pattern: &Address{
|
||||
Agent: "any", Role: "any", Project: "any", Task: "any",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no match different agent",
|
||||
address: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
pattern: &Address{
|
||||
Agent: "agent2", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "path prefix match",
|
||||
address: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
Path: "config/app.yaml",
|
||||
},
|
||||
pattern: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
Path: "config",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "path no match",
|
||||
address: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
Path: "src/main.go",
|
||||
},
|
||||
pattern: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
Path: "config",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.address.Matches(tt.pattern)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Matches() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressIsWildcard(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address *Address
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "no wildcards",
|
||||
address: &Address{
|
||||
Agent: "agent1", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "agent wildcard",
|
||||
address: &Address{
|
||||
Agent: "any", Role: "developer", Project: "project1", Task: "task1",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "all wildcards",
|
||||
address: &Address{
|
||||
Agent: "any", Role: "any", Project: "any", Task: "any",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.address.IsWildcard()
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsWildcard() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressClone(t *testing.T) {
|
||||
original := &Address{
|
||||
Agent: "agent1",
|
||||
Role: "developer",
|
||||
Project: "project1",
|
||||
Task: "task1",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalSpecific,
|
||||
Count: 5,
|
||||
},
|
||||
Path: "src/main.go",
|
||||
Raw: "ucxl://agent1:developer@project1:task1/*~5/src/main.go",
|
||||
}
|
||||
|
||||
cloned := original.Clone()
|
||||
|
||||
// Test that clone is equal to original
|
||||
if !reflect.DeepEqual(original, cloned) {
|
||||
t.Errorf("Clone() should create identical copy")
|
||||
}
|
||||
|
||||
// Test that modifying clone doesn't affect original
|
||||
cloned.Agent = "different"
|
||||
if original.Agent == cloned.Agent {
|
||||
t.Errorf("Clone() should create independent copy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemporalSegmentString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
segment TemporalSegment
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "latest",
|
||||
segment: TemporalSegment{Type: TemporalLatest},
|
||||
expected: "*^",
|
||||
},
|
||||
{
|
||||
name: "any",
|
||||
segment: TemporalSegment{Type: TemporalAny},
|
||||
expected: "*~",
|
||||
},
|
||||
{
|
||||
name: "specific version",
|
||||
segment: TemporalSegment{Type: TemporalSpecific, Count: 7},
|
||||
expected: "*~7",
|
||||
},
|
||||
{
|
||||
name: "backward navigation",
|
||||
segment: TemporalSegment{Type: TemporalRelative, Direction: DirectionBackward, Count: 3},
|
||||
expected: "~~3",
|
||||
},
|
||||
{
|
||||
name: "forward navigation",
|
||||
segment: TemporalSegment{Type: TemporalRelative, Direction: DirectionForward, Count: 2},
|
||||
expected: "^^2",
|
||||
},
|
||||
{
|
||||
name: "unknown type defaults to latest",
|
||||
segment: TemporalSegment{Type: "unknown"},
|
||||
expected: "*^",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.segment.String()
|
||||
if result != tt.expected {
|
||||
t.Errorf("String() = %s, want %s", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkParseAddress(b *testing.B) {
|
||||
address := "ucxl://agent1:developer@project1:task1/*~/path/to/file.txt"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := Parse(address)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAddressString(b *testing.B) {
|
||||
addr := &Address{
|
||||
Agent: "agent1",
|
||||
Role: "developer",
|
||||
Project: "project1",
|
||||
Task: "task1",
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalSpecific,
|
||||
Count: 5,
|
||||
},
|
||||
Path: "src/main.go",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = addr.String()
|
||||
}
|
||||
}
|
||||
333
pkg/ucxl/codes.go
Normal file
333
pkg/ucxl/codes.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package ucxl
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// UCXLCode represents a standardized UCXL response/error code
|
||||
type UCXLCode string
|
||||
|
||||
// Standard UCXL response codes
|
||||
const (
|
||||
// Success codes (2xx range)
|
||||
CodeSuccess UCXLCode = "UCXL-200-SUCCESS"
|
||||
CodeCreated UCXLCode = "UCXL-201-CREATED"
|
||||
CodeAccepted UCXLCode = "UCXL-202-ACCEPTED"
|
||||
CodeNoContent UCXLCode = "UCXL-204-NO_CONTENT"
|
||||
|
||||
// Client error codes (4xx range)
|
||||
CodeBadRequest UCXLCode = "UCXL-400-BAD_REQUEST"
|
||||
CodeInvalidAddress UCXLCode = "UCXL-400-INVALID_ADDRESS"
|
||||
CodeInvalidPayload UCXLCode = "UCXL-400-INVALID_PAYLOAD"
|
||||
CodeUnauthorized UCXLCode = "UCXL-401-UNAUTHORIZED"
|
||||
CodeForbidden UCXLCode = "UCXL-403-FORBIDDEN"
|
||||
CodeNotFound UCXLCode = "UCXL-404-NOT_FOUND"
|
||||
CodeMethodNotAllowed UCXLCode = "UCXL-405-METHOD_NOT_ALLOWED"
|
||||
CodeConflict UCXLCode = "UCXL-409-CONFLICT"
|
||||
CodeUnprocessable UCXLCode = "UCXL-422-UNPROCESSABLE"
|
||||
CodeTooManyRequests UCXLCode = "UCXL-429-TOO_MANY_REQUESTS"
|
||||
|
||||
// Server error codes (5xx range)
|
||||
CodeInternalError UCXLCode = "UCXL-500-INTERNAL_ERROR"
|
||||
CodeNotImplemented UCXLCode = "UCXL-501-NOT_IMPLEMENTED"
|
||||
CodeBadGateway UCXLCode = "UCXL-502-BAD_GATEWAY"
|
||||
CodeServiceUnavailable UCXLCode = "UCXL-503-SERVICE_UNAVAILABLE"
|
||||
CodeGatewayTimeout UCXLCode = "UCXL-504-GATEWAY_TIMEOUT"
|
||||
|
||||
// UCXI-specific codes
|
||||
CodeResolutionFailed UCXLCode = "UCXL-404-RESOLUTION_FAILED"
|
||||
CodeStorageFailed UCXLCode = "UCXL-500-STORAGE_FAILED"
|
||||
CodeAnnounceFailed UCXLCode = "UCXL-500-ANNOUNCE_FAILED"
|
||||
CodeNavigationFailed UCXLCode = "UCXL-422-NAVIGATION_FAILED"
|
||||
CodeTemporalInvalid UCXLCode = "UCXL-400-TEMPORAL_INVALID"
|
||||
|
||||
// Role-based collaboration codes
|
||||
CodeCollaborationFailed UCXLCode = "UCXL-500-COLLABORATION_FAILED"
|
||||
CodeInvalidRole UCXLCode = "UCXL-400-INVALID_ROLE"
|
||||
CodeExpertiseNotAvailable UCXLCode = "UCXL-404-EXPERTISE_NOT_AVAILABLE"
|
||||
CodeMentorshipUnavailable UCXLCode = "UCXL-404-MENTORSHIP_UNAVAILABLE"
|
||||
CodeProjectNotFound UCXLCode = "UCXL-404-PROJECT_NOT_FOUND"
|
||||
CodeCollaborationTimeout UCXLCode = "UCXL-408-COLLABORATION_TIMEOUT"
|
||||
)
|
||||
|
||||
// UCXLResponse represents a standardized UCXL success response
|
||||
type UCXLResponse struct {
|
||||
Response UCXLResponseData `json:"response"`
|
||||
}
|
||||
|
||||
// UCXLResponseData contains the actual response data
|
||||
type UCXLResponseData struct {
|
||||
Code UCXLCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Details interface{} `json:"details,omitempty"`
|
||||
RequestID string `json:"request_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// UCXLError represents a standardized UCXL error response
|
||||
type UCXLError struct {
|
||||
Error UCXLErrorData `json:"error"`
|
||||
}
|
||||
|
||||
// UCXLErrorData contains the actual error data
|
||||
type UCXLErrorData struct {
|
||||
Code UCXLCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details interface{} `json:"details,omitempty"`
|
||||
Source string `json:"source"`
|
||||
Path string `json:"path"`
|
||||
RequestID string `json:"request_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Cause *UCXLError `json:"cause,omitempty"`
|
||||
}
|
||||
|
||||
// ResponseBuilder helps build standardized UCXL responses
|
||||
type ResponseBuilder struct {
|
||||
requestID string
|
||||
source string
|
||||
}
|
||||
|
||||
// NewResponseBuilder creates a new response builder
|
||||
func NewResponseBuilder(requestID string, source string) *ResponseBuilder {
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
if source == "" {
|
||||
source = "ucxi-server"
|
||||
}
|
||||
return &ResponseBuilder{
|
||||
requestID: requestID,
|
||||
source: source,
|
||||
}
|
||||
}
|
||||
|
||||
// Success creates a standardized success response
|
||||
func (rb *ResponseBuilder) Success(code UCXLCode, message string, data interface{}) *UCXLResponse {
|
||||
return &UCXLResponse{
|
||||
Response: UCXLResponseData{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
RequestID: rb.requestID,
|
||||
Timestamp: time.Now().UTC(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SuccessWithDetails creates a success response with additional details
|
||||
func (rb *ResponseBuilder) SuccessWithDetails(code UCXLCode, message string, data interface{}, details interface{}) *UCXLResponse {
|
||||
return &UCXLResponse{
|
||||
Response: UCXLResponseData{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
Details: details,
|
||||
RequestID: rb.requestID,
|
||||
Timestamp: time.Now().UTC(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Error creates a standardized error response
|
||||
func (rb *ResponseBuilder) Error(code UCXLCode, message string, path string) *UCXLError {
|
||||
return &UCXLError{
|
||||
Error: UCXLErrorData{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Source: rb.source,
|
||||
Path: path,
|
||||
RequestID: rb.requestID,
|
||||
Timestamp: time.Now().UTC(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorWithDetails creates an error response with additional details
|
||||
func (rb *ResponseBuilder) ErrorWithDetails(code UCXLCode, message string, path string, details interface{}) *UCXLError {
|
||||
return &UCXLError{
|
||||
Error: UCXLErrorData{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
Source: rb.source,
|
||||
Path: path,
|
||||
RequestID: rb.requestID,
|
||||
Timestamp: time.Now().UTC(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorWithCause creates an error response with a causal chain
|
||||
func (rb *ResponseBuilder) ErrorWithCause(code UCXLCode, message string, path string, cause *UCXLError) *UCXLError {
|
||||
return &UCXLError{
|
||||
Error: UCXLErrorData{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Source: rb.source,
|
||||
Path: path,
|
||||
RequestID: rb.requestID,
|
||||
Timestamp: time.Now().UTC(),
|
||||
Cause: cause,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for common responses
|
||||
|
||||
// OK creates a standard 200 OK response
|
||||
func (rb *ResponseBuilder) OK(data interface{}) *UCXLResponse {
|
||||
return rb.Success(CodeSuccess, "Request completed successfully", data)
|
||||
}
|
||||
|
||||
// Created creates a standard 201 Created response
|
||||
func (rb *ResponseBuilder) Created(data interface{}) *UCXLResponse {
|
||||
return rb.Success(CodeCreated, "Resource created successfully", data)
|
||||
}
|
||||
|
||||
// NoContent creates a standard 204 No Content response
|
||||
func (rb *ResponseBuilder) NoContent() *UCXLResponse {
|
||||
return rb.Success(CodeNoContent, "Request completed with no content", nil)
|
||||
}
|
||||
|
||||
// BadRequest creates a standard 400 Bad Request error
|
||||
func (rb *ResponseBuilder) BadRequest(message string, path string) *UCXLError {
|
||||
return rb.Error(CodeBadRequest, message, path)
|
||||
}
|
||||
|
||||
// InvalidAddress creates a UCXL-specific invalid address error
|
||||
func (rb *ResponseBuilder) InvalidAddress(message string, path string, addressDetails interface{}) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeInvalidAddress, message, path, map[string]interface{}{
|
||||
"field": "address",
|
||||
"address": addressDetails,
|
||||
})
|
||||
}
|
||||
|
||||
// NotFound creates a standard 404 Not Found error
|
||||
func (rb *ResponseBuilder) NotFound(message string, path string) *UCXLError {
|
||||
return rb.Error(CodeNotFound, message, path)
|
||||
}
|
||||
|
||||
// Unprocessable creates a standard 422 Unprocessable Entity error
|
||||
func (rb *ResponseBuilder) Unprocessable(message string, path string, validationErrors interface{}) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeUnprocessable, message, path, map[string]interface{}{
|
||||
"validation_errors": validationErrors,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalError creates a standard 500 Internal Server Error
|
||||
func (rb *ResponseBuilder) InternalError(message string, path string) *UCXLError {
|
||||
return rb.Error(CodeInternalError, message, path)
|
||||
}
|
||||
|
||||
// MethodNotAllowed creates a standard 405 Method Not Allowed error
|
||||
func (rb *ResponseBuilder) MethodNotAllowed(allowedMethods []string, path string) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeMethodNotAllowed, "Method not allowed", path, map[string]interface{}{
|
||||
"allowed_methods": allowedMethods,
|
||||
})
|
||||
}
|
||||
|
||||
// Collaboration-specific error builders
|
||||
|
||||
// InvalidRole creates a UCXL-specific invalid role error
|
||||
func (rb *ResponseBuilder) InvalidRole(message string, path string, roleDetails interface{}) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeInvalidRole, message, path, map[string]interface{}{
|
||||
"field": "role",
|
||||
"role_details": roleDetails,
|
||||
})
|
||||
}
|
||||
|
||||
// ExpertiseNotAvailable creates a UCXL-specific expertise not available error
|
||||
func (rb *ResponseBuilder) ExpertiseNotAvailable(message string, path string, expertiseDetails interface{}) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeExpertiseNotAvailable, message, path, map[string]interface{}{
|
||||
"requested_expertise": expertiseDetails,
|
||||
"suggestion": "Try requesting more general expertise or check available experts",
|
||||
})
|
||||
}
|
||||
|
||||
// ProjectNotFound creates a UCXL-specific project not found error
|
||||
func (rb *ResponseBuilder) ProjectNotFound(message string, path string, projectID string) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeProjectNotFound, message, path, map[string]interface{}{
|
||||
"field": "project_id",
|
||||
"project_id": projectID,
|
||||
"suggestion": "Verify the project ID is correct and accessible",
|
||||
})
|
||||
}
|
||||
|
||||
// CollaborationTimeout creates a UCXL-specific collaboration timeout error
|
||||
func (rb *ResponseBuilder) CollaborationTimeout(message string, path string, timeoutDetails interface{}) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeCollaborationTimeout, message, path, map[string]interface{}{
|
||||
"timeout_reason": timeoutDetails,
|
||||
"suggestion": "Retry the collaboration request or check system load",
|
||||
})
|
||||
}
|
||||
|
||||
// CollaborationFailed creates a UCXL-specific collaboration failure error
|
||||
func (rb *ResponseBuilder) CollaborationFailed(message string, path string, failureDetails interface{}) *UCXLError {
|
||||
return rb.ErrorWithDetails(CodeCollaborationFailed, message, path, map[string]interface{}{
|
||||
"failure_details": failureDetails,
|
||||
"suggestion": "Check system status and pubsub connectivity",
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// GetHTTPStatus maps UCXL codes to HTTP status codes
|
||||
func GetHTTPStatus(code UCXLCode) int {
|
||||
switch code {
|
||||
case CodeSuccess:
|
||||
return 200
|
||||
case CodeCreated:
|
||||
return 201
|
||||
case CodeAccepted:
|
||||
return 202
|
||||
case CodeNoContent:
|
||||
return 204
|
||||
case CodeBadRequest, CodeInvalidAddress, CodeInvalidPayload, CodeTemporalInvalid, CodeInvalidRole:
|
||||
return 400
|
||||
case CodeUnauthorized:
|
||||
return 401
|
||||
case CodeForbidden:
|
||||
return 403
|
||||
case CodeNotFound, CodeResolutionFailed, CodeExpertiseNotAvailable, CodeMentorshipUnavailable, CodeProjectNotFound:
|
||||
return 404
|
||||
case CodeCollaborationTimeout:
|
||||
return 408
|
||||
case CodeMethodNotAllowed:
|
||||
return 405
|
||||
case CodeConflict:
|
||||
return 409
|
||||
case CodeUnprocessable, CodeNavigationFailed:
|
||||
return 422
|
||||
case CodeTooManyRequests:
|
||||
return 429
|
||||
case CodeInternalError, CodeStorageFailed, CodeAnnounceFailed, CodeCollaborationFailed:
|
||||
return 500
|
||||
case CodeNotImplemented:
|
||||
return 501
|
||||
case CodeBadGateway:
|
||||
return 502
|
||||
case CodeServiceUnavailable:
|
||||
return 503
|
||||
case CodeGatewayTimeout:
|
||||
return 504
|
||||
default:
|
||||
return 500
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID creates a unique request ID
|
||||
func generateRequestID() string {
|
||||
// Simple UUID-like generator for request IDs
|
||||
return time.Now().Format("20060102-150405") + "-" + randomString(8)
|
||||
}
|
||||
|
||||
// randomString generates a random string of the specified length
|
||||
func randomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[time.Now().UnixNano()%(int64(len(charset)))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
376
pkg/ucxl/decision_publisher.go
Normal file
376
pkg/ucxl/decision_publisher.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package ucxl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"chorus.services/bzzz/pkg/config"
|
||||
"chorus.services/bzzz/pkg/storage"
|
||||
)
|
||||
|
||||
// DecisionPublisher handles publishing task completion decisions to encrypted DHT storage
|
||||
type DecisionPublisher struct {
|
||||
ctx context.Context
|
||||
config *config.Config
|
||||
dhtStorage storage.UCXLStorage
|
||||
nodeID string
|
||||
agentName string
|
||||
}
|
||||
|
||||
// NewDecisionPublisher creates a new decision publisher
|
||||
func NewDecisionPublisher(
|
||||
ctx context.Context,
|
||||
config *config.Config,
|
||||
dhtStorage storage.UCXLStorage,
|
||||
nodeID string,
|
||||
agentName string,
|
||||
) *DecisionPublisher {
|
||||
return &DecisionPublisher{
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
dhtStorage: dhtStorage,
|
||||
nodeID: nodeID,
|
||||
agentName: agentName,
|
||||
}
|
||||
}
|
||||
|
||||
// TaskDecision represents a decision made by an agent upon task completion
|
||||
type TaskDecision struct {
|
||||
Agent string `json:"agent"`
|
||||
Role string `json:"role"`
|
||||
Project string `json:"project"`
|
||||
Task string `json:"task"`
|
||||
Decision string `json:"decision"`
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
FilesModified []string `json:"files_modified,omitempty"`
|
||||
LinesChanged int `json:"lines_changed,omitempty"`
|
||||
TestResults *TestResults `json:"test_results,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
NextSteps []string `json:"next_steps,omitempty"`
|
||||
}
|
||||
|
||||
// TestResults captures test execution results
|
||||
type TestResults struct {
|
||||
Passed int `json:"passed"`
|
||||
Failed int `json:"failed"`
|
||||
Skipped int `json:"skipped"`
|
||||
Coverage float64 `json:"coverage,omitempty"`
|
||||
FailedTests []string `json:"failed_tests,omitempty"`
|
||||
}
|
||||
|
||||
// PublishTaskDecision publishes a task completion decision to the DHT
|
||||
func (dp *DecisionPublisher) PublishTaskDecision(decision *TaskDecision) error {
|
||||
// Ensure required fields
|
||||
if decision.Agent == "" {
|
||||
decision.Agent = dp.agentName
|
||||
}
|
||||
if decision.Role == "" {
|
||||
decision.Role = dp.config.Agent.Role
|
||||
}
|
||||
if decision.Project == "" {
|
||||
decision.Project = "default-project" // TODO: Add project field to config
|
||||
}
|
||||
if decision.Timestamp.IsZero() {
|
||||
decision.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
log.Printf("📤 Publishing task decision: %s/%s/%s", decision.Agent, decision.Project, decision.Task)
|
||||
|
||||
// Generate UCXL address
|
||||
ucxlAddress, err := dp.generateUCXLAddress(decision)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate UCXL address: %w", err)
|
||||
}
|
||||
|
||||
// Serialize decision content
|
||||
decisionContent, err := json.MarshalIndent(decision, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize decision: %w", err)
|
||||
}
|
||||
|
||||
// Store in encrypted DHT
|
||||
err = dp.dhtStorage.StoreUCXLContent(
|
||||
ucxlAddress,
|
||||
decisionContent,
|
||||
decision.Role,
|
||||
"decision",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store decision in DHT: %w", err)
|
||||
}
|
||||
|
||||
// Announce content availability
|
||||
if err := dp.dhtStorage.AnnounceContent(ucxlAddress); err != nil {
|
||||
log.Printf("⚠️ Failed to announce decision content: %v", err)
|
||||
// Don't fail the publish operation for announcement failure
|
||||
}
|
||||
|
||||
log.Printf("✅ Published task decision: %s", ucxlAddress)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishTaskCompletion publishes a simple task completion without detailed context
|
||||
func (dp *DecisionPublisher) PublishTaskCompletion(
|
||||
taskName string,
|
||||
success bool,
|
||||
summary string,
|
||||
filesModified []string,
|
||||
) error {
|
||||
decision := &TaskDecision{
|
||||
Task: taskName,
|
||||
Decision: summary,
|
||||
Success: success,
|
||||
FilesModified: filesModified,
|
||||
Context: map[string]interface{}{
|
||||
"completion_type": "basic",
|
||||
"node_id": dp.nodeID,
|
||||
},
|
||||
}
|
||||
|
||||
return dp.PublishTaskDecision(decision)
|
||||
}
|
||||
|
||||
// PublishCodeDecision publishes a coding decision with technical context
|
||||
func (dp *DecisionPublisher) PublishCodeDecision(
|
||||
taskName string,
|
||||
decision string,
|
||||
filesModified []string,
|
||||
linesChanged int,
|
||||
testResults *TestResults,
|
||||
dependencies []string,
|
||||
) error {
|
||||
taskDecision := &TaskDecision{
|
||||
Task: taskName,
|
||||
Decision: decision,
|
||||
Success: testResults == nil || testResults.Failed == 0,
|
||||
FilesModified: filesModified,
|
||||
LinesChanged: linesChanged,
|
||||
TestResults: testResults,
|
||||
Dependencies: dependencies,
|
||||
Context: map[string]interface{}{
|
||||
"decision_type": "code",
|
||||
"node_id": dp.nodeID,
|
||||
"language": dp.detectLanguage(filesModified),
|
||||
},
|
||||
}
|
||||
|
||||
return dp.PublishTaskDecision(taskDecision)
|
||||
}
|
||||
|
||||
// PublishArchitecturalDecision publishes a high-level architectural decision
|
||||
func (dp *DecisionPublisher) PublishArchitecturalDecision(
|
||||
taskName string,
|
||||
decision string,
|
||||
rationale string,
|
||||
alternatives []string,
|
||||
implications []string,
|
||||
nextSteps []string,
|
||||
) error {
|
||||
taskDecision := &TaskDecision{
|
||||
Task: taskName,
|
||||
Decision: decision,
|
||||
Success: true,
|
||||
NextSteps: nextSteps,
|
||||
Context: map[string]interface{}{
|
||||
"decision_type": "architecture",
|
||||
"rationale": rationale,
|
||||
"alternatives": alternatives,
|
||||
"implications": implications,
|
||||
"node_id": dp.nodeID,
|
||||
},
|
||||
}
|
||||
|
||||
return dp.PublishTaskDecision(taskDecision)
|
||||
}
|
||||
|
||||
// generateUCXLAddress creates a UCXL address for the decision
|
||||
func (dp *DecisionPublisher) generateUCXLAddress(decision *TaskDecision) (string, error) {
|
||||
address := &Address{
|
||||
Agent: decision.Agent,
|
||||
Role: decision.Role,
|
||||
Project: decision.Project,
|
||||
Task: decision.Task,
|
||||
TemporalSegment: TemporalSegment{
|
||||
Type: TemporalLatest, // Latest decision for this agent/role/project/task
|
||||
},
|
||||
}
|
||||
|
||||
return address.String(), nil
|
||||
}
|
||||
|
||||
// detectLanguage attempts to detect the programming language from modified files
|
||||
func (dp *DecisionPublisher) detectLanguage(files []string) string {
|
||||
languageMap := map[string]string{
|
||||
".go": "go",
|
||||
".py": "python",
|
||||
".js": "javascript",
|
||||
".ts": "typescript",
|
||||
".rs": "rust",
|
||||
".java": "java",
|
||||
".c": "c",
|
||||
".cpp": "cpp",
|
||||
".cs": "csharp",
|
||||
".php": "php",
|
||||
".rb": "ruby",
|
||||
".yaml": "yaml",
|
||||
".yml": "yaml",
|
||||
".json": "json",
|
||||
".md": "markdown",
|
||||
}
|
||||
|
||||
languageCounts := make(map[string]int)
|
||||
|
||||
for _, file := range files {
|
||||
for ext, lang := range languageMap {
|
||||
if len(file) > len(ext) && file[len(file)-len(ext):] == ext {
|
||||
languageCounts[lang]++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the most common language
|
||||
maxCount := 0
|
||||
primaryLanguage := "unknown"
|
||||
for lang, count := range languageCounts {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
primaryLanguage = lang
|
||||
}
|
||||
}
|
||||
|
||||
return primaryLanguage
|
||||
}
|
||||
|
||||
// QueryRecentDecisions retrieves recent decisions from the DHT
|
||||
func (dp *DecisionPublisher) QueryRecentDecisions(
|
||||
agent string,
|
||||
role string,
|
||||
project string,
|
||||
limit int,
|
||||
since time.Time,
|
||||
) ([]*storage.UCXLMetadata, error) {
|
||||
query := &storage.SearchQuery{
|
||||
Agent: agent,
|
||||
Role: role,
|
||||
Project: project,
|
||||
ContentType: "decision",
|
||||
CreatedAfter: since,
|
||||
Limit: limit,
|
||||
}
|
||||
|
||||
return dp.dhtStorage.SearchContent(query)
|
||||
}
|
||||
|
||||
// GetDecisionContent retrieves and decrypts a specific decision
|
||||
func (dp *DecisionPublisher) GetDecisionContent(ucxlAddress string) (*TaskDecision, error) {
|
||||
content, metadata, err := dp.dhtStorage.RetrieveUCXLContent(ucxlAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve decision content: %w", err)
|
||||
}
|
||||
|
||||
var decision TaskDecision
|
||||
if err := json.Unmarshal(content, &decision); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse decision content: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("📥 Retrieved decision: %s (creator: %s)", ucxlAddress, metadata.CreatorRole)
|
||||
return &decision, nil
|
||||
}
|
||||
|
||||
// SubscribeToDecisions sets up a subscription to new decisions (placeholder for future pubsub)
|
||||
func (dp *DecisionPublisher) SubscribeToDecisions(
|
||||
roleFilter string,
|
||||
callback func(*TaskDecision, *storage.UCXLMetadata),
|
||||
) error {
|
||||
// This is a placeholder for future pubsub implementation
|
||||
// For now, we'll implement a simple polling mechanism
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
lastCheck := time.Now()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-dp.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Query for recent decisions
|
||||
decisions, err := dp.QueryRecentDecisions("", roleFilter, "", 10, lastCheck)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to query recent decisions: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process new decisions
|
||||
for _, metadata := range decisions {
|
||||
decision, err := dp.GetDecisionContent(metadata.Address)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to get decision content: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
callback(decision, metadata)
|
||||
}
|
||||
|
||||
lastCheck = time.Now()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("🔔 Subscribed to decisions for role: %s", roleFilter)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishSystemStatus publishes current system status as a decision
|
||||
func (dp *DecisionPublisher) PublishSystemStatus(
|
||||
status string,
|
||||
metrics map[string]interface{},
|
||||
healthChecks map[string]bool,
|
||||
) error {
|
||||
decision := &TaskDecision{
|
||||
Task: "system_status",
|
||||
Decision: status,
|
||||
Success: dp.allHealthChecksPass(healthChecks),
|
||||
Context: map[string]interface{}{
|
||||
"decision_type": "system",
|
||||
"metrics": metrics,
|
||||
"health_checks": healthChecks,
|
||||
"node_id": dp.nodeID,
|
||||
},
|
||||
}
|
||||
|
||||
return dp.PublishTaskDecision(decision)
|
||||
}
|
||||
|
||||
// allHealthChecksPass checks if all health checks are passing
|
||||
func (dp *DecisionPublisher) allHealthChecksPass(healthChecks map[string]bool) bool {
|
||||
for _, passing := range healthChecks {
|
||||
if !passing {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPublisherMetrics returns metrics about the decision publisher
|
||||
func (dp *DecisionPublisher) GetPublisherMetrics() map[string]interface{} {
|
||||
dhtMetrics := dp.dhtStorage.GetMetrics()
|
||||
|
||||
return map[string]interface{}{
|
||||
"node_id": dp.nodeID,
|
||||
"agent_name": dp.agentName,
|
||||
"current_role": dp.config.Agent.Role,
|
||||
"project": "default-project", // TODO: Add project field to config
|
||||
"dht_metrics": dhtMetrics,
|
||||
"last_publish": time.Now(), // This would be tracked in a real implementation
|
||||
}
|
||||
}
|
||||
247
pkg/ucxl/parser.go
Normal file
247
pkg/ucxl/parser.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package ucxl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UCXLAddress represents a parsed UCXL address
|
||||
type UCXLAddress struct {
|
||||
Raw string `json:"raw"`
|
||||
Agent string `json:"agent"` // Agent ID or "*" for any
|
||||
Role string `json:"role"` // Agent role or "*" for any
|
||||
Project string `json:"project"` // Project identifier or "*" for any
|
||||
Task string `json:"task"` // Task identifier or "*" for any
|
||||
Path string `json:"path"` // Resource path (optional)
|
||||
Temporal string `json:"temporal"` // Temporal navigation (optional)
|
||||
}
|
||||
|
||||
// UCXLAddressRegex matches valid UCXL address format
|
||||
// Format: ucxl://agent:role@project:task/path*temporal/
|
||||
var UCXLAddressRegex = regexp.MustCompile(`^ucxl://([^:]+):([^@]+)@([^:]+):([^/]+)(/[^*]*)?(\*[^/]*)?/?$`)
|
||||
|
||||
// ParseUCXLAddress parses a UCXL address string into its components
|
||||
func ParseUCXLAddress(address string) (*UCXLAddress, error) {
|
||||
if address == "" {
|
||||
return nil, fmt.Errorf("empty UCXL address")
|
||||
}
|
||||
|
||||
// Check if it starts with ucxl://
|
||||
if !strings.HasPrefix(address, "ucxl://") {
|
||||
return nil, fmt.Errorf("invalid UCXL address: must start with 'ucxl://'")
|
||||
}
|
||||
|
||||
// Parse using regex
|
||||
matches := UCXLAddressRegex.FindStringSubmatch(address)
|
||||
if matches == nil {
|
||||
return nil, fmt.Errorf("invalid UCXL address format: %s", address)
|
||||
}
|
||||
|
||||
ucxlAddr := &UCXLAddress{
|
||||
Raw: address,
|
||||
Agent: matches[1],
|
||||
Role: matches[2],
|
||||
Project: matches[3],
|
||||
Task: matches[4],
|
||||
Path: strings.TrimPrefix(matches[5], "/"),
|
||||
Temporal: strings.TrimPrefix(matches[6], "*"),
|
||||
}
|
||||
|
||||
// Validate components
|
||||
if err := validateUCXLComponents(ucxlAddr); err != nil {
|
||||
return nil, fmt.Errorf("invalid UCXL address: %w", err)
|
||||
}
|
||||
|
||||
return ucxlAddr, nil
|
||||
}
|
||||
|
||||
// validateUCXLComponents validates individual components of a UCXL address
|
||||
func validateUCXLComponents(addr *UCXLAddress) error {
|
||||
// Agent can be any non-empty string or "*"
|
||||
if addr.Agent == "" {
|
||||
return fmt.Errorf("agent cannot be empty")
|
||||
}
|
||||
|
||||
// Role can be any non-empty string or "*"
|
||||
if addr.Role == "" {
|
||||
return fmt.Errorf("role cannot be empty")
|
||||
}
|
||||
|
||||
// Project can be any non-empty string or "*"
|
||||
if addr.Project == "" {
|
||||
return fmt.Errorf("project cannot be empty")
|
||||
}
|
||||
|
||||
// Task can be any non-empty string or "*"
|
||||
if addr.Task == "" {
|
||||
return fmt.Errorf("task cannot be empty")
|
||||
}
|
||||
|
||||
// Path is optional, but if present should be valid
|
||||
if addr.Path != "" {
|
||||
// URL decode and validate path
|
||||
decoded, err := url.PathUnescape(addr.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid path encoding: %w", err)
|
||||
}
|
||||
addr.Path = decoded
|
||||
}
|
||||
|
||||
// Temporal is optional
|
||||
if addr.Temporal != "" {
|
||||
// Validate temporal navigation syntax
|
||||
if !isValidTemporal(addr.Temporal) {
|
||||
return fmt.Errorf("invalid temporal navigation syntax: %s", addr.Temporal)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidTemporal validates temporal navigation syntax
|
||||
func isValidTemporal(temporal string) bool {
|
||||
// Valid temporal patterns:
|
||||
// ^/ - latest version
|
||||
// ~/ - earliest version
|
||||
// @timestamp - specific timestamp
|
||||
// ~n/ - n versions back
|
||||
// ^n/ - n versions forward
|
||||
validPatterns := []string{
|
||||
`^\^/?$`, // ^/ or ^
|
||||
`^~/?$`, // ~/ or ~
|
||||
`^@\d+/?$`, // @timestamp
|
||||
`^~\d+/?$`, // ~n versions back
|
||||
`^\^\d+/?$`, // ^n versions forward
|
||||
}
|
||||
|
||||
for _, pattern := range validPatterns {
|
||||
matched, _ := regexp.MatchString(pattern, temporal)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateUCXLAddress creates a UCXL address from components
|
||||
func GenerateUCXLAddress(agent, role, project, task, path, temporal string) (string, error) {
|
||||
// Validate required components
|
||||
if agent == "" || role == "" || project == "" || task == "" {
|
||||
return "", fmt.Errorf("agent, role, project, and task are required")
|
||||
}
|
||||
|
||||
// Build address
|
||||
address := fmt.Sprintf("ucxl://%s:%s@%s:%s", agent, role, project, task)
|
||||
|
||||
// Add path if provided
|
||||
if path != "" {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
// URL encode the path
|
||||
encodedPath := url.PathEscape(path)
|
||||
address += encodedPath
|
||||
}
|
||||
|
||||
// Add temporal navigation if provided
|
||||
if temporal != "" {
|
||||
if !strings.HasPrefix(temporal, "*") {
|
||||
temporal = "*" + temporal
|
||||
}
|
||||
address += temporal
|
||||
}
|
||||
|
||||
// Always end with /
|
||||
if !strings.HasSuffix(address, "/") {
|
||||
address += "/"
|
||||
}
|
||||
|
||||
// Validate the generated address
|
||||
_, err := ParseUCXLAddress(address)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generated invalid UCXL address: %w", err)
|
||||
}
|
||||
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// IsValidUCXLAddress checks if a string is a valid UCXL address
|
||||
func IsValidUCXLAddress(address string) bool {
|
||||
_, err := ParseUCXLAddress(address)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// NormalizeUCXLAddress normalizes a UCXL address to standard format
|
||||
func NormalizeUCXLAddress(address string) (string, error) {
|
||||
parsed, err := ParseUCXLAddress(address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Regenerate in standard format
|
||||
return GenerateUCXLAddress(
|
||||
parsed.Agent,
|
||||
parsed.Role,
|
||||
parsed.Project,
|
||||
parsed.Task,
|
||||
parsed.Path,
|
||||
parsed.Temporal,
|
||||
)
|
||||
}
|
||||
|
||||
// MatchesPattern checks if an address matches a pattern (supports wildcards)
|
||||
func (addr *UCXLAddress) MatchesPattern(pattern *UCXLAddress) bool {
|
||||
// Check agent
|
||||
if pattern.Agent != "*" && pattern.Agent != addr.Agent {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check role
|
||||
if pattern.Role != "*" && pattern.Role != addr.Role {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check project
|
||||
if pattern.Project != "*" && pattern.Project != addr.Project {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check task
|
||||
if pattern.Task != "*" && pattern.Task != addr.Task {
|
||||
return false
|
||||
}
|
||||
|
||||
// Path matching (prefix-based if pattern ends with *)
|
||||
if pattern.Path != "" {
|
||||
if strings.HasSuffix(pattern.Path, "*") {
|
||||
prefix := strings.TrimSuffix(pattern.Path, "*")
|
||||
if !strings.HasPrefix(addr.Path, prefix) {
|
||||
return false
|
||||
}
|
||||
} else if pattern.Path != addr.Path {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ToMap converts the UCXL address to a map for JSON serialization
|
||||
func (addr *UCXLAddress) ToMap() map[string]string {
|
||||
return map[string]string{
|
||||
"raw": addr.Raw,
|
||||
"agent": addr.Agent,
|
||||
"role": addr.Role,
|
||||
"project": addr.Project,
|
||||
"task": addr.Task,
|
||||
"path": addr.Path,
|
||||
"temporal": addr.Temporal,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the UCXL address
|
||||
func (addr *UCXLAddress) String() string {
|
||||
return addr.Raw
|
||||
}
|
||||
377
pkg/ucxl/temporal.go
Normal file
377
pkg/ucxl/temporal.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package ucxl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TemporalNavigator handles temporal navigation operations within UCXL addresses
|
||||
type TemporalNavigator struct {
|
||||
// Navigation history for tracking traversal paths
|
||||
history []NavigationStep
|
||||
|
||||
// Current position in version space
|
||||
currentVersion int
|
||||
maxVersion int
|
||||
|
||||
// Version metadata
|
||||
versions map[int]VersionInfo
|
||||
}
|
||||
|
||||
// NavigationStep represents a single step in temporal navigation history
|
||||
type NavigationStep struct {
|
||||
FromVersion int `json:"from_version"`
|
||||
ToVersion int `json:"to_version"`
|
||||
Operation string `json:"operation"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// VersionInfo contains metadata about a specific version
|
||||
type VersionInfo struct {
|
||||
Version int `json:"version"`
|
||||
Created time.Time `json:"created"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// NavigationResult represents the result of a temporal navigation operation
|
||||
type NavigationResult struct {
|
||||
Success bool `json:"success"`
|
||||
TargetVersion int `json:"target_version"`
|
||||
PreviousVersion int `json:"previous_version"`
|
||||
VersionInfo *VersionInfo `json:"version_info,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TemporalConstraintError represents an error when temporal constraints are violated
|
||||
type TemporalConstraintError struct {
|
||||
Operation string `json:"operation"`
|
||||
RequestedStep int `json:"requested_step"`
|
||||
CurrentVersion int `json:"current_version"`
|
||||
MaxVersion int `json:"max_version"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e TemporalConstraintError) Error() string {
|
||||
return fmt.Sprintf("temporal constraint violation: %s (current: %d, max: %d, requested: %d)",
|
||||
e.Message, e.CurrentVersion, e.MaxVersion, e.RequestedStep)
|
||||
}
|
||||
|
||||
// NewTemporalNavigator creates a new temporal navigator
|
||||
func NewTemporalNavigator(maxVersion int) *TemporalNavigator {
|
||||
if maxVersion < 0 {
|
||||
maxVersion = 0
|
||||
}
|
||||
|
||||
return &TemporalNavigator{
|
||||
history: make([]NavigationStep, 0),
|
||||
currentVersion: maxVersion, // Start at latest version
|
||||
maxVersion: maxVersion,
|
||||
versions: make(map[int]VersionInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate performs temporal navigation based on the temporal segment
|
||||
func (tn *TemporalNavigator) Navigate(segment TemporalSegment) (*NavigationResult, error) {
|
||||
previousVersion := tn.currentVersion
|
||||
var targetVersion int
|
||||
var err error
|
||||
|
||||
step := NavigationStep{
|
||||
FromVersion: previousVersion,
|
||||
Timestamp: time.Now(),
|
||||
Operation: segment.String(),
|
||||
}
|
||||
|
||||
switch segment.Type {
|
||||
case TemporalLatest:
|
||||
targetVersion = tn.maxVersion
|
||||
err = tn.navigateToVersion(targetVersion)
|
||||
|
||||
case TemporalAny:
|
||||
// For "any", we stay at current version (no navigation)
|
||||
targetVersion = tn.currentVersion
|
||||
|
||||
case TemporalSpecific:
|
||||
targetVersion = segment.Count
|
||||
err = tn.navigateToVersion(targetVersion)
|
||||
|
||||
case TemporalRelative:
|
||||
targetVersion, err = tn.navigateRelative(segment.Direction, segment.Count)
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unknown temporal type: %v", segment.Type)
|
||||
}
|
||||
|
||||
// Record the navigation step
|
||||
step.ToVersion = targetVersion
|
||||
step.Success = err == nil
|
||||
if err != nil {
|
||||
step.Error = err.Error()
|
||||
}
|
||||
tn.history = append(tn.history, step)
|
||||
|
||||
result := &NavigationResult{
|
||||
Success: err == nil,
|
||||
TargetVersion: targetVersion,
|
||||
PreviousVersion: previousVersion,
|
||||
}
|
||||
|
||||
// Include version info if available
|
||||
if versionInfo, exists := tn.versions[targetVersion]; exists {
|
||||
result.VersionInfo = &versionInfo
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// navigateToVersion navigates directly to a specific version
|
||||
func (tn *TemporalNavigator) navigateToVersion(version int) error {
|
||||
if version < 0 {
|
||||
return &TemporalConstraintError{
|
||||
Operation: "navigate_to_version",
|
||||
RequestedStep: version,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "cannot navigate to negative version",
|
||||
}
|
||||
}
|
||||
|
||||
if version > tn.maxVersion {
|
||||
return &TemporalConstraintError{
|
||||
Operation: "navigate_to_version",
|
||||
RequestedStep: version,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "cannot navigate beyond latest version",
|
||||
}
|
||||
}
|
||||
|
||||
tn.currentVersion = version
|
||||
return nil
|
||||
}
|
||||
|
||||
// navigateRelative performs relative navigation (forward/backward)
|
||||
func (tn *TemporalNavigator) navigateRelative(direction Direction, count int) (int, error) {
|
||||
if count < 0 {
|
||||
return tn.currentVersion, &TemporalConstraintError{
|
||||
Operation: fmt.Sprintf("navigate_relative_%s", direction),
|
||||
RequestedStep: count,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "navigation count cannot be negative",
|
||||
}
|
||||
}
|
||||
|
||||
var targetVersion int
|
||||
|
||||
switch direction {
|
||||
case DirectionBackward:
|
||||
targetVersion = tn.currentVersion - count
|
||||
if targetVersion < 0 {
|
||||
return tn.currentVersion, &TemporalConstraintError{
|
||||
Operation: "navigate_backward",
|
||||
RequestedStep: count,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "cannot navigate before first version (version 0)",
|
||||
}
|
||||
}
|
||||
|
||||
case DirectionForward:
|
||||
targetVersion = tn.currentVersion + count
|
||||
if targetVersion > tn.maxVersion {
|
||||
return tn.currentVersion, &TemporalConstraintError{
|
||||
Operation: "navigate_forward",
|
||||
RequestedStep: count,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "cannot navigate beyond latest version",
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return tn.currentVersion, fmt.Errorf("unknown navigation direction: %v", direction)
|
||||
}
|
||||
|
||||
tn.currentVersion = targetVersion
|
||||
return targetVersion, nil
|
||||
}
|
||||
|
||||
// GetCurrentVersion returns the current version position
|
||||
func (tn *TemporalNavigator) GetCurrentVersion() int {
|
||||
return tn.currentVersion
|
||||
}
|
||||
|
||||
// GetMaxVersion returns the maximum available version
|
||||
func (tn *TemporalNavigator) GetMaxVersion() int {
|
||||
return tn.maxVersion
|
||||
}
|
||||
|
||||
// SetMaxVersion updates the maximum version (e.g., when new versions are created)
|
||||
func (tn *TemporalNavigator) SetMaxVersion(maxVersion int) error {
|
||||
if maxVersion < 0 {
|
||||
return fmt.Errorf("max version cannot be negative")
|
||||
}
|
||||
|
||||
tn.maxVersion = maxVersion
|
||||
|
||||
// If current version is now beyond max, adjust it
|
||||
if tn.currentVersion > tn.maxVersion {
|
||||
tn.currentVersion = tn.maxVersion
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHistory returns the navigation history
|
||||
func (tn *TemporalNavigator) GetHistory() []NavigationStep {
|
||||
// Return a copy to prevent modification
|
||||
history := make([]NavigationStep, len(tn.history))
|
||||
copy(history, tn.history)
|
||||
return history
|
||||
}
|
||||
|
||||
// ClearHistory clears the navigation history
|
||||
func (tn *TemporalNavigator) ClearHistory() {
|
||||
tn.history = make([]NavigationStep, 0)
|
||||
}
|
||||
|
||||
// GetLastNavigation returns the most recent navigation step
|
||||
func (tn *TemporalNavigator) GetLastNavigation() *NavigationStep {
|
||||
if len(tn.history) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
last := tn.history[len(tn.history)-1]
|
||||
return &last
|
||||
}
|
||||
|
||||
// SetVersionInfo sets metadata for a specific version
|
||||
func (tn *TemporalNavigator) SetVersionInfo(version int, info VersionInfo) {
|
||||
info.Version = version // Ensure consistency
|
||||
tn.versions[version] = info
|
||||
}
|
||||
|
||||
// GetVersionInfo retrieves metadata for a specific version
|
||||
func (tn *TemporalNavigator) GetVersionInfo(version int) (*VersionInfo, bool) {
|
||||
info, exists := tn.versions[version]
|
||||
if exists {
|
||||
return &info, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetAllVersions returns metadata for all known versions
|
||||
func (tn *TemporalNavigator) GetAllVersions() map[int]VersionInfo {
|
||||
// Return a copy to prevent modification
|
||||
result := make(map[int]VersionInfo)
|
||||
for k, v := range tn.versions {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CanNavigateBackward returns true if backward navigation is possible
|
||||
func (tn *TemporalNavigator) CanNavigateBackward(count int) bool {
|
||||
return tn.currentVersion-count >= 0
|
||||
}
|
||||
|
||||
// CanNavigateForward returns true if forward navigation is possible
|
||||
func (tn *TemporalNavigator) CanNavigateForward(count int) bool {
|
||||
return tn.currentVersion+count <= tn.maxVersion
|
||||
}
|
||||
|
||||
// Reset resets the navigator to the latest version and clears history
|
||||
func (tn *TemporalNavigator) Reset() {
|
||||
tn.currentVersion = tn.maxVersion
|
||||
tn.ClearHistory()
|
||||
}
|
||||
|
||||
// Clone creates a copy of the temporal navigator
|
||||
func (tn *TemporalNavigator) Clone() *TemporalNavigator {
|
||||
clone := &TemporalNavigator{
|
||||
currentVersion: tn.currentVersion,
|
||||
maxVersion: tn.maxVersion,
|
||||
history: make([]NavigationStep, len(tn.history)),
|
||||
versions: make(map[int]VersionInfo),
|
||||
}
|
||||
|
||||
// Copy history
|
||||
copy(clone.history, tn.history)
|
||||
|
||||
// Copy version info
|
||||
for k, v := range tn.versions {
|
||||
clone.versions[k] = v
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// ValidateTemporalSegment validates a temporal segment against current navigator state
|
||||
func (tn *TemporalNavigator) ValidateTemporalSegment(segment TemporalSegment) error {
|
||||
switch segment.Type {
|
||||
case TemporalLatest:
|
||||
// Always valid
|
||||
return nil
|
||||
|
||||
case TemporalAny:
|
||||
// Always valid
|
||||
return nil
|
||||
|
||||
case TemporalSpecific:
|
||||
if segment.Count < 0 || segment.Count > tn.maxVersion {
|
||||
return &TemporalConstraintError{
|
||||
Operation: "validate_specific",
|
||||
RequestedStep: segment.Count,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "specific version out of valid range",
|
||||
}
|
||||
}
|
||||
|
||||
case TemporalRelative:
|
||||
if segment.Count < 0 {
|
||||
return fmt.Errorf("relative navigation count cannot be negative")
|
||||
}
|
||||
|
||||
switch segment.Direction {
|
||||
case DirectionBackward:
|
||||
if !tn.CanNavigateBackward(segment.Count) {
|
||||
return &TemporalConstraintError{
|
||||
Operation: "validate_backward",
|
||||
RequestedStep: segment.Count,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "backward navigation would go before first version",
|
||||
}
|
||||
}
|
||||
|
||||
case DirectionForward:
|
||||
if !tn.CanNavigateForward(segment.Count) {
|
||||
return &TemporalConstraintError{
|
||||
Operation: "validate_forward",
|
||||
RequestedStep: segment.Count,
|
||||
CurrentVersion: tn.currentVersion,
|
||||
MaxVersion: tn.maxVersion,
|
||||
Message: "forward navigation would go beyond latest version",
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown temporal direction: %v", segment.Direction)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown temporal type: %v", segment.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
623
pkg/ucxl/temporal_test.go
Normal file
623
pkg/ucxl/temporal_test.go
Normal file
@@ -0,0 +1,623 @@
|
||||
package ucxl
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewTemporalNavigator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
maxVersion int
|
||||
expectedMax int
|
||||
expectedCurrent int
|
||||
}{
|
||||
{
|
||||
name: "positive max version",
|
||||
maxVersion: 10,
|
||||
expectedMax: 10,
|
||||
expectedCurrent: 10,
|
||||
},
|
||||
{
|
||||
name: "zero max version",
|
||||
maxVersion: 0,
|
||||
expectedMax: 0,
|
||||
expectedCurrent: 0,
|
||||
},
|
||||
{
|
||||
name: "negative max version defaults to 0",
|
||||
maxVersion: -5,
|
||||
expectedMax: 0,
|
||||
expectedCurrent: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
nav := NewTemporalNavigator(tt.maxVersion)
|
||||
|
||||
if nav.GetMaxVersion() != tt.expectedMax {
|
||||
t.Errorf("GetMaxVersion() = %d, want %d", nav.GetMaxVersion(), tt.expectedMax)
|
||||
}
|
||||
|
||||
if nav.GetCurrentVersion() != tt.expectedCurrent {
|
||||
t.Errorf("GetCurrentVersion() = %d, want %d", nav.GetCurrentVersion(), tt.expectedCurrent)
|
||||
}
|
||||
|
||||
if nav.GetHistory() == nil {
|
||||
t.Error("History should be initialized")
|
||||
}
|
||||
|
||||
if len(nav.GetHistory()) != 0 {
|
||||
t.Error("History should be empty initially")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigateLatest(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
|
||||
// Navigate to version 5 first
|
||||
nav.currentVersion = 5
|
||||
|
||||
segment := TemporalSegment{Type: TemporalLatest}
|
||||
result, err := nav.Navigate(segment)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Navigate() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
t.Error("Navigation should be successful")
|
||||
}
|
||||
|
||||
if result.TargetVersion != 10 {
|
||||
t.Errorf("TargetVersion = %d, want 10", result.TargetVersion)
|
||||
}
|
||||
|
||||
if result.PreviousVersion != 5 {
|
||||
t.Errorf("PreviousVersion = %d, want 5", result.PreviousVersion)
|
||||
}
|
||||
|
||||
if nav.GetCurrentVersion() != 10 {
|
||||
t.Errorf("Current version = %d, want 10", nav.GetCurrentVersion())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigateAny(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
nav.currentVersion = 5
|
||||
|
||||
segment := TemporalSegment{Type: TemporalAny}
|
||||
result, err := nav.Navigate(segment)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Navigate() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
t.Error("Navigation should be successful")
|
||||
}
|
||||
|
||||
if result.TargetVersion != 5 {
|
||||
t.Errorf("TargetVersion = %d, want 5 (should stay at current)", result.TargetVersion)
|
||||
}
|
||||
|
||||
if nav.GetCurrentVersion() != 5 {
|
||||
t.Errorf("Current version = %d, want 5", nav.GetCurrentVersion())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigateSpecific(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version int
|
||||
shouldError bool
|
||||
expectedPos int
|
||||
}{
|
||||
{
|
||||
name: "valid version",
|
||||
version: 7,
|
||||
shouldError: false,
|
||||
expectedPos: 7,
|
||||
},
|
||||
{
|
||||
name: "version 0",
|
||||
version: 0,
|
||||
shouldError: false,
|
||||
expectedPos: 0,
|
||||
},
|
||||
{
|
||||
name: "max version",
|
||||
version: 10,
|
||||
shouldError: false,
|
||||
expectedPos: 10,
|
||||
},
|
||||
{
|
||||
name: "negative version",
|
||||
version: -1,
|
||||
shouldError: true,
|
||||
expectedPos: 10, // Should stay at original position
|
||||
},
|
||||
{
|
||||
name: "version beyond max",
|
||||
version: 15,
|
||||
shouldError: true,
|
||||
expectedPos: 10, // Should stay at original position
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
nav.Reset() // Reset to max version
|
||||
|
||||
segment := TemporalSegment{
|
||||
Type: TemporalSpecific,
|
||||
Count: tt.version,
|
||||
}
|
||||
|
||||
result, err := nav.Navigate(segment)
|
||||
|
||||
if tt.shouldError {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("Result should indicate failure")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !result.Success {
|
||||
t.Error("Result should indicate success")
|
||||
}
|
||||
}
|
||||
|
||||
if nav.GetCurrentVersion() != tt.expectedPos {
|
||||
t.Errorf("Current version = %d, want %d", nav.GetCurrentVersion(), tt.expectedPos)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigateBackward(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
nav.currentVersion = 5
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
count int
|
||||
shouldError bool
|
||||
expectedPos int
|
||||
}{
|
||||
{
|
||||
name: "valid backward navigation",
|
||||
count: 2,
|
||||
shouldError: false,
|
||||
expectedPos: 3,
|
||||
},
|
||||
{
|
||||
name: "backward to version 0",
|
||||
count: 5,
|
||||
shouldError: false,
|
||||
expectedPos: 0,
|
||||
},
|
||||
{
|
||||
name: "backward beyond version 0",
|
||||
count: 10,
|
||||
shouldError: true,
|
||||
expectedPos: 5, // Should stay at original position
|
||||
},
|
||||
{
|
||||
name: "negative count",
|
||||
count: -1,
|
||||
shouldError: true,
|
||||
expectedPos: 5, // Should stay at original position
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
nav.currentVersion = 5 // Reset position
|
||||
|
||||
segment := TemporalSegment{
|
||||
Type: TemporalRelative,
|
||||
Direction: DirectionBackward,
|
||||
Count: tt.count,
|
||||
}
|
||||
|
||||
result, err := nav.Navigate(segment)
|
||||
|
||||
if tt.shouldError {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("Result should indicate failure")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !result.Success {
|
||||
t.Error("Result should indicate success")
|
||||
}
|
||||
}
|
||||
|
||||
if nav.GetCurrentVersion() != tt.expectedPos {
|
||||
t.Errorf("Current version = %d, want %d", nav.GetCurrentVersion(), tt.expectedPos)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigateForward(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
nav.currentVersion = 5
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
count int
|
||||
shouldError bool
|
||||
expectedPos int
|
||||
}{
|
||||
{
|
||||
name: "valid forward navigation",
|
||||
count: 3,
|
||||
shouldError: false,
|
||||
expectedPos: 8,
|
||||
},
|
||||
{
|
||||
name: "forward to max version",
|
||||
count: 5,
|
||||
shouldError: false,
|
||||
expectedPos: 10,
|
||||
},
|
||||
{
|
||||
name: "forward beyond max version",
|
||||
count: 10,
|
||||
shouldError: true,
|
||||
expectedPos: 5, // Should stay at original position
|
||||
},
|
||||
{
|
||||
name: "negative count",
|
||||
count: -1,
|
||||
shouldError: true,
|
||||
expectedPos: 5, // Should stay at original position
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
nav.currentVersion = 5 // Reset position
|
||||
|
||||
segment := TemporalSegment{
|
||||
Type: TemporalRelative,
|
||||
Direction: DirectionForward,
|
||||
Count: tt.count,
|
||||
}
|
||||
|
||||
result, err := nav.Navigate(segment)
|
||||
|
||||
if tt.shouldError {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("Result should indicate failure")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !result.Success {
|
||||
t.Error("Result should indicate success")
|
||||
}
|
||||
}
|
||||
|
||||
if nav.GetCurrentVersion() != tt.expectedPos {
|
||||
t.Errorf("Current version = %d, want %d", nav.GetCurrentVersion(), tt.expectedPos)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigationHistory(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
|
||||
// Perform several navigations
|
||||
segments := []TemporalSegment{
|
||||
{Type: TemporalSpecific, Count: 5},
|
||||
{Type: TemporalRelative, Direction: DirectionBackward, Count: 2},
|
||||
{Type: TemporalLatest},
|
||||
}
|
||||
|
||||
for _, segment := range segments {
|
||||
nav.Navigate(segment)
|
||||
}
|
||||
|
||||
history := nav.GetHistory()
|
||||
if len(history) != 3 {
|
||||
t.Errorf("History length = %d, want 3", len(history))
|
||||
}
|
||||
|
||||
// Check that all steps are recorded
|
||||
for i, step := range history {
|
||||
if step.Operation == "" {
|
||||
t.Errorf("Step %d should have operation recorded", i)
|
||||
}
|
||||
if step.Timestamp.IsZero() {
|
||||
t.Errorf("Step %d should have timestamp", i)
|
||||
}
|
||||
if !step.Success {
|
||||
t.Errorf("Step %d should be successful", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Test clear history
|
||||
nav.ClearHistory()
|
||||
if len(nav.GetHistory()) != 0 {
|
||||
t.Error("History should be empty after ClearHistory()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMaxVersion(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
nav.currentVersion = 5
|
||||
|
||||
// Test increasing max version
|
||||
err := nav.SetMaxVersion(15)
|
||||
if err != nil {
|
||||
t.Errorf("SetMaxVersion(15) error = %v, want nil", err)
|
||||
}
|
||||
if nav.GetMaxVersion() != 15 {
|
||||
t.Errorf("Max version = %d, want 15", nav.GetMaxVersion())
|
||||
}
|
||||
if nav.GetCurrentVersion() != 5 {
|
||||
t.Errorf("Current version should remain at 5, got %d", nav.GetCurrentVersion())
|
||||
}
|
||||
|
||||
// Test decreasing max version below current
|
||||
err = nav.SetMaxVersion(3)
|
||||
if err != nil {
|
||||
t.Errorf("SetMaxVersion(3) error = %v, want nil", err)
|
||||
}
|
||||
if nav.GetMaxVersion() != 3 {
|
||||
t.Errorf("Max version = %d, want 3", nav.GetMaxVersion())
|
||||
}
|
||||
if nav.GetCurrentVersion() != 3 {
|
||||
t.Errorf("Current version should be adjusted to 3, got %d", nav.GetCurrentVersion())
|
||||
}
|
||||
|
||||
// Test negative max version
|
||||
err = nav.SetMaxVersion(-1)
|
||||
if err == nil {
|
||||
t.Error("SetMaxVersion(-1) should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionInfo(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
|
||||
info := VersionInfo{
|
||||
Version: 5,
|
||||
Created: time.Now(),
|
||||
Author: "test-author",
|
||||
Description: "test version",
|
||||
Tags: []string{"stable", "release"},
|
||||
}
|
||||
|
||||
// Set version info
|
||||
nav.SetVersionInfo(5, info)
|
||||
|
||||
// Retrieve version info
|
||||
retrievedInfo, exists := nav.GetVersionInfo(5)
|
||||
if !exists {
|
||||
t.Error("Version info should exist")
|
||||
}
|
||||
if retrievedInfo.Author != info.Author {
|
||||
t.Errorf("Author = %s, want %s", retrievedInfo.Author, info.Author)
|
||||
}
|
||||
|
||||
// Test non-existent version
|
||||
_, exists = nav.GetVersionInfo(99)
|
||||
if exists {
|
||||
t.Error("Version info should not exist for version 99")
|
||||
}
|
||||
|
||||
// Test GetAllVersions
|
||||
allVersions := nav.GetAllVersions()
|
||||
if len(allVersions) != 1 {
|
||||
t.Errorf("All versions count = %d, want 1", len(allVersions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanNavigate(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
nav.currentVersion = 5
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction string
|
||||
count int
|
||||
expected bool
|
||||
}{
|
||||
{"can go backward 3", "backward", 3, true},
|
||||
{"can go backward 5", "backward", 5, true},
|
||||
{"cannot go backward 6", "backward", 6, false},
|
||||
{"can go forward 3", "forward", 3, true},
|
||||
{"can go forward 5", "forward", 5, true},
|
||||
{"cannot go forward 6", "forward", 6, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var result bool
|
||||
if tt.direction == "backward" {
|
||||
result = nav.CanNavigateBackward(tt.count)
|
||||
} else {
|
||||
result = nav.CanNavigateForward(tt.count)
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("Can navigate %s %d = %v, want %v", tt.direction, tt.count, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTemporalSegment(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
nav.currentVersion = 5
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
segment TemporalSegment
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "latest is valid",
|
||||
segment: TemporalSegment{Type: TemporalLatest},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "any is valid",
|
||||
segment: TemporalSegment{Type: TemporalAny},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "valid specific version",
|
||||
segment: TemporalSegment{Type: TemporalSpecific, Count: 7},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "specific version out of range",
|
||||
segment: TemporalSegment{Type: TemporalSpecific, Count: 15},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "valid backward navigation",
|
||||
segment: TemporalSegment{Type: TemporalRelative, Direction: DirectionBackward, Count: 3},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "backward navigation out of range",
|
||||
segment: TemporalSegment{Type: TemporalRelative, Direction: DirectionBackward, Count: 10},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "valid forward navigation",
|
||||
segment: TemporalSegment{Type: TemporalRelative, Direction: DirectionForward, Count: 3},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "forward navigation out of range",
|
||||
segment: TemporalSegment{Type: TemporalRelative, Direction: DirectionForward, Count: 10},
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := nav.ValidateTemporalSegment(tt.segment)
|
||||
|
||||
if tt.shouldError {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigatorClone(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
nav.currentVersion = 5
|
||||
|
||||
// Add some version info and history
|
||||
nav.SetVersionInfo(5, VersionInfo{Author: "test"})
|
||||
nav.Navigate(TemporalSegment{Type: TemporalLatest})
|
||||
|
||||
cloned := nav.Clone()
|
||||
|
||||
// Test that basic properties are cloned
|
||||
if cloned.GetCurrentVersion() != nav.GetCurrentVersion() {
|
||||
t.Error("Current version should be cloned")
|
||||
}
|
||||
if cloned.GetMaxVersion() != nav.GetMaxVersion() {
|
||||
t.Error("Max version should be cloned")
|
||||
}
|
||||
|
||||
// Test that history is cloned
|
||||
originalHistory := nav.GetHistory()
|
||||
clonedHistory := cloned.GetHistory()
|
||||
if !reflect.DeepEqual(originalHistory, clonedHistory) {
|
||||
t.Error("History should be cloned")
|
||||
}
|
||||
|
||||
// Test that version info is cloned
|
||||
originalVersions := nav.GetAllVersions()
|
||||
clonedVersions := cloned.GetAllVersions()
|
||||
if !reflect.DeepEqual(originalVersions, clonedVersions) {
|
||||
t.Error("Version info should be cloned")
|
||||
}
|
||||
|
||||
// Test independence - modifying clone shouldn't affect original
|
||||
cloned.currentVersion = 0
|
||||
if nav.GetCurrentVersion() == cloned.GetCurrentVersion() {
|
||||
t.Error("Clone should be independent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLastNavigation(t *testing.T) {
|
||||
nav := NewTemporalNavigator(10)
|
||||
|
||||
// Initially should return nil
|
||||
last := nav.GetLastNavigation()
|
||||
if last != nil {
|
||||
t.Error("GetLastNavigation() should return nil when no navigation has occurred")
|
||||
}
|
||||
|
||||
// After navigation should return the step
|
||||
segment := TemporalSegment{Type: TemporalSpecific, Count: 5}
|
||||
nav.Navigate(segment)
|
||||
|
||||
last = nav.GetLastNavigation()
|
||||
if last == nil {
|
||||
t.Error("GetLastNavigation() should return the last navigation step")
|
||||
}
|
||||
if last.Operation != segment.String() {
|
||||
t.Errorf("Operation = %s, want %s", last.Operation, segment.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkNavigate(b *testing.B) {
|
||||
nav := NewTemporalNavigator(100)
|
||||
segment := TemporalSegment{Type: TemporalSpecific, Count: 50}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
nav.Navigate(segment)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidateTemporalSegment(b *testing.B) {
|
||||
nav := NewTemporalNavigator(100)
|
||||
nav.currentVersion = 50
|
||||
segment := TemporalSegment{Type: TemporalRelative, Direction: DirectionBackward, Count: 10}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
nav.ValidateTemporalSegment(segment)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user