Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 2 additions & 30 deletions pkg/chainstate/chainstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func Load(path string) (*State, error) {
}
return nil, err
}

var state State
if err := json.Unmarshal(data, &state); err == nil {
if state.Projects == nil {
Expand All @@ -44,39 +45,10 @@ func Load(path string) (*State, error) {
return &state, nil
}

// Fallback to old format migration
var old map[string]int
if err := json.Unmarshal(data, &old); err == nil {
return migrateOldState(old), nil
}

// Unmarshal failed completely, return empty state
// Unmarshal failed, return empty state
return &State{Projects: make(map[string]*ProjectState)}, nil
}

func migrateOldState(old map[string]int) *State {
s := &State{Projects: make(map[string]*ProjectState)}
for key, value := range old {
parts := strings.SplitN(key, "|", 3)
if len(parts) == 2 {
// root|name
root := parts[0]
name := parts[1]
p := getOrCreateProject(s, root)
p.Chains[name] = value
} else if len(parts) == 3 && parts[1] == "@active" {
// root|@active|name
root := parts[0]
name := parts[2]
if value > 0 {
p := getOrCreateProject(s, root)
p.ActiveChain = name
}
}
}
return s
}

func getOrCreateProject(state *State, root string) *ProjectState {
cleanRoot := filepath.Clean(root)
p, ok := state.Projects[cleanRoot]
Expand Down
106 changes: 106 additions & 0 deletions pkg/chainstate/chainstate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package chainstate

import (
"os"
"path/filepath"
"testing"
)

func TestLoadSaveComplexState(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "state.json")

state := &State{Projects: make(map[string]*ProjectState)}

// Set up some initial state
SetActive("/my/project1", "chainA", state)
SetStep("/my/project1", "chainA", 2, state)
SetStep("/my/project1", "chainB", 5, state)

SetActive("/my/project2", "chainX", state)

err := Save(path, state)
if err != nil {
t.Fatalf("Failed to save state: %v", err)
}

// Load it back
loaded, err := Load(path)
if err != nil {
t.Fatalf("Failed to load state: %v", err)
}

if ActiveName("/my/project1", loaded) != "chainA" {
t.Errorf("Expected active chain 'chainA', got '%s'", ActiveName("/my/project1", loaded))
}
if GetStep("/my/project1", "chainA", loaded) != 2 {
t.Errorf("Expected step 2, got %d", GetStep("/my/project1", "chainA", loaded))
}
if GetStep("/my/project1", "chainB", loaded) != 5 {
t.Errorf("Expected step 5, got %d", GetStep("/my/project1", "chainB", loaded))
}
if ActiveName("/my/project2", loaded) != "chainX" {
t.Errorf("Expected active chain 'chainX', got '%s'", ActiveName("/my/project2", loaded))
}
}

func TestClear(t *testing.T) {
state := &State{Projects: make(map[string]*ProjectState)}
SetStep("/root", "chain1", 1, state)
SetActive("/root", "chain1", state)
SetStep("/root", "chain2", 2, state)

// Clear specific chain that is active
Clear("/root", "chain1", state)

if GetStep("/root", "chain1", state) != 0 {
t.Errorf("Expected chain1 step to be 0")
}
if ActiveName("/root", state) != "" {
t.Errorf("Expected active name to be cleared")
}
if GetStep("/root", "chain2", state) != 2 {
t.Errorf("Expected chain2 step to remain 2")
}

// Clear whole project
Clear("/root", "", state)
if _, exists := state.Projects["/root"]; exists {
t.Errorf("Expected project /root to be completely removed")
}
}

func TestLoadEdgeCases(t *testing.T) {
tmpDir := t.TempDir()

// 1. Missing file should return empty state
state, err := Load(filepath.Join(tmpDir, "missing.json"))
if err != nil {
t.Errorf("Missing file should not error, got: %v", err)
}
if state == nil || state.Projects == nil {
t.Errorf("Missing file should return initialized empty state")
}

// 2. Corrupt file should return empty state without crashing
path := filepath.Join(tmpDir, "corrupt.json")
os.WriteFile(path, []byte("{ invalid json"), 0o644)
state, err = Load(path)
if err != nil {
t.Errorf("Corrupt file should not error out completely, should recover empty state. Got error: %v", err)
}
if state == nil || state.Projects == nil {
t.Errorf("Corrupt file should return initialized empty state")
}

// 3. Empty file
pathEmpty := filepath.Join(tmpDir, "empty.json")
os.WriteFile(pathEmpty, []byte(""), 0o644)
state, err = Load(pathEmpty)
if err != nil {
t.Errorf("Empty file should not error, got: %v", err)
}
if state == nil || state.Projects == nil {
t.Errorf("Empty file should return initialized empty state")
}
}
Loading