feat: model selection at spawn (POST /api/spawn model -> LVMH_MODEL -> bridge createAgentSession); ops prepare resets ops session (fresh context per prepare)
This commit is contained in:
@@ -575,3 +575,110 @@ func TestParseEnabledModels(t *testing.T) {
|
||||
t.Fatal("invalid json must yield nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPISpawnModelValidation(t *testing.T) {
|
||||
ts, _ := newSpawnAPIServer(t)
|
||||
auth := testToken
|
||||
// invalid model spec -> 400 before any spawn work
|
||||
if code, body := apiReq(t, http.MethodPost, ts.URL+"/api/spawn", auth,
|
||||
`{"repo":"g/p","model":"no-slash"}`); code != http.StatusBadRequest {
|
||||
t.Fatalf("bad model = %d %s, want 400", code, body)
|
||||
}
|
||||
if code, _ := apiReq(t, http.MethodPost, ts.URL+"/api/spawn", auth,
|
||||
`{"repo":"g/p","model":"zai-renaud/glm-5.3"}`); code != http.StatusCreated {
|
||||
t.Fatal("valid provider/model-id must pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIHandlersStoreFailuresCovered(t *testing.T) {
|
||||
ts, store, _ := newTestServerHub(t)
|
||||
auth := testToken
|
||||
_ = store.Close()
|
||||
|
||||
paths := []struct {
|
||||
method, path, body string
|
||||
want int
|
||||
}{
|
||||
{"GET", "/api/sessions", "", http.StatusOK}, // swallows store errors by design
|
||||
{"GET", "/api/sessions/s1/events", "", http.StatusInternalServerError},
|
||||
{"GET", "/api/sessions/s1/stats", "", http.StatusInternalServerError},
|
||||
{"POST", "/api/sessions/s1/prompt", `{"message":"hi"}`, http.StatusConflict},
|
||||
{"POST", "/api/sessions/s1/abort", "", http.StatusConflict},
|
||||
{"POST", "/api/sessions/s1/model", `{"provider":"p","modelId":"m"}`, http.StatusConflict},
|
||||
{"PATCH", "/api/sessions/s1", `{"name":"n"}`, http.StatusInternalServerError},
|
||||
{"GET", "/api/stats", "", http.StatusInternalServerError},
|
||||
}
|
||||
for _, tc := range paths {
|
||||
code, body := apiReq(t, tc.method, ts.URL+tc.path, auth, tc.body)
|
||||
if code != tc.want {
|
||||
t.Errorf("%s %s = %d %s, want %d", tc.method, tc.path, code, body, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelCatalogFallbackPaths(t *testing.T) {
|
||||
ts, _ := newTestServer(t)
|
||||
dir := t.TempDir()
|
||||
minimal := filepath.Join(dir, "minimal-models.json")
|
||||
if err := os.WriteFile(minimal, []byte(`{"providers":{"p":{"models":[{"id":"m1","name":"M1"}]}}}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings := filepath.Join(dir, "settings.json")
|
||||
if err := os.WriteFile(settings, []byte(`{"enabledModels":["anthropic/claude-x","p/m1","junk"]}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("explicit-file-when-baked-missing", func(t *testing.T) {
|
||||
t.Setenv(envModelsFile, minimal)
|
||||
code, body := apiReq(t, http.MethodGet, ts.URL+"/api/model-catalog", testToken, "")
|
||||
if code != http.StatusOK || !strings.Contains(body, `"id":"m1"`) {
|
||||
t.Fatalf("explicit = %d %s", code, body)
|
||||
}
|
||||
})
|
||||
t.Run("baked-preferred-with-settings-merge", func(t *testing.T) {
|
||||
t.Setenv(envModelsFile, "")
|
||||
// pretend the baked dotfiles models.json exists by pointing the
|
||||
// test at a temp file via the same stat+read the handler uses.
|
||||
t.Setenv("LVMH_TEST_BAKED_MODELS", minimal)
|
||||
t.Setenv(envSettingsFile, "LVMH_SETTINGS_FILE")
|
||||
t.Setenv("LVMH_SETTINGS_FILE", settings)
|
||||
code, body := apiReq(t, http.MethodGet, ts.URL+"/api/model-catalog", testToken, "")
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("merge = %d %s", code, body)
|
||||
}
|
||||
if !strings.Contains(body, "claude-x") {
|
||||
t.Fatalf("enabledModels not merged: %s", body)
|
||||
}
|
||||
if strings.Count(body, `"id":"m1"`) != 1 {
|
||||
t.Fatalf("dedupe broken: %s", body)
|
||||
}
|
||||
})
|
||||
t.Run("unreadable-file-500", func(t *testing.T) {
|
||||
t.Setenv(envModelsFile, filepath.Join(dir, "nope.json"))
|
||||
code, _ := apiReq(t, http.MethodGet, ts.URL+"/api/model-catalog", testToken, "")
|
||||
if code != http.StatusInternalServerError {
|
||||
t.Fatalf("missing file = %d, want 500", code)
|
||||
}
|
||||
})
|
||||
t.Run("garbage-json-500", func(t *testing.T) {
|
||||
bad := filepath.Join(dir, "bad.json")
|
||||
if err := os.WriteFile(bad, []byte("{{"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv(envModelsFile, bad)
|
||||
code, _ := apiReq(t, http.MethodGet, ts.URL+"/api/model-catalog", testToken, "")
|
||||
if code != http.StatusInternalServerError {
|
||||
t.Fatalf("garbage = %d, want 500", code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenameAndSetModelBodyValidation(t *testing.T) {
|
||||
ts, _ := newTestServer(t)
|
||||
if code, _ := apiReq(t, http.MethodPatch, ts.URL+"/api/sessions/s1", testToken, "not json"); code != http.StatusBadRequest {
|
||||
t.Fatalf("bad rename body = %d, want 400", code)
|
||||
}
|
||||
if code, _ := apiReq(t, http.MethodPost, ts.URL+"/api/sessions/s1/model", testToken, "nope"); code != http.StatusBadRequest {
|
||||
t.Fatalf("bad model body = %d, want 400", code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user