fix(14): WR-05 import every key of an object-shaped models repeater
A repeater saved with non-sequential keys was read only at keys 0 to len-1, so a model at a higher key was dropped while the import reported success and refused to run again. Every key is now read, numeric keys in ascending order.
This commit is contained in:
@@ -2,12 +2,15 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -150,11 +153,11 @@ func repeaterItems(raw []byte) ([]map[string]any, error) {
|
||||
case []any:
|
||||
list = t
|
||||
case map[string]any:
|
||||
// A repeater saved with non-sequential keys decodes as an object.
|
||||
for i := 0; i < len(t); i++ {
|
||||
if item, ok := t[strconv.Itoa(i)]; ok {
|
||||
list = append(list, item)
|
||||
}
|
||||
// A repeater saved with non-sequential keys decodes as an object
|
||||
// ({"0":…,"2":…} after a middle item was deleted). Every key is
|
||||
// read, numeric keys in ascending order, then any other key.
|
||||
for _, k := range repeaterKeys(t) {
|
||||
list = append(list, t[k])
|
||||
}
|
||||
case nil:
|
||||
default:
|
||||
@@ -169,6 +172,26 @@ func repeaterItems(raw []byte) ([]map[string]any, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// repeaterKeys orders an object repeater's keys: integers ascending by
|
||||
// value first, then the other keys in byte order.
|
||||
func repeaterKeys(t map[string]any) []string {
|
||||
keys := slices.Collect(maps.Keys(t))
|
||||
slices.SortFunc(keys, func(a, b string) int {
|
||||
ai, aerr := strconv.Atoi(a)
|
||||
bi, berr := strconv.Atoi(b)
|
||||
switch {
|
||||
case aerr == nil && berr == nil:
|
||||
return cmp.Compare(ai, bi)
|
||||
case aerr == nil:
|
||||
return -1
|
||||
case berr == nil:
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
func modelFromItem(item map[string]any) models.AiModel {
|
||||
row := models.AiModel{
|
||||
Name: str(item["name"]),
|
||||
|
||||
@@ -151,6 +151,22 @@ func TestGolemImportCommand(t *testing.T) {
|
||||
if err := db.Exec(`DELETE FROM golem15_golem_models`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A repeater whose middle item was deleted keeps every remaining key,
|
||||
// in numeric order (WR-05).
|
||||
sparse := write("sparse.json", `{"models":{"10":{"name":"Ten","model":"m10","api_key":"k10"},"0":{"name":"Zero","model":"m0","api_key":"k0"},"2":{"name":"Two","model":"m2","api_key":"k2"}}}`)
|
||||
if out, err := run(app, sparse); err != nil || !strings.Contains(out, "Imported 3 AI models.") {
|
||||
t.Fatalf("sparse import = %q %v", out, err)
|
||||
}
|
||||
rows = nil
|
||||
if err := db.Order("sort_order").Find(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 3 || rows[0].Name != "Zero" || rows[1].Name != "Two" || rows[2].Name != "Ten" {
|
||||
t.Fatalf("sparse rows = %+v", rows)
|
||||
}
|
||||
if err := db.Exec(`DELETE FROM golem15_golem_models`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, body := range map[string]string{"empty.json": `{"models":[]}`, "null.json": `{"models":null}`} {
|
||||
if out, err := run(app, write(name, body)); err != nil || !strings.Contains(out, "no AI models") {
|
||||
t.Errorf("%s = %q %v", name, out, err)
|
||||
|
||||
Reference in New Issue
Block a user