Files
ragflow/internal/cli/admin_variables_test.go
Jake Armstrong 93d3deb5e4 Fix admin CLI system variable commands (#14956)
## What

Fixes #12409.

Implements admin CLI support for:

- `list vars;`
- `show var <name-or-prefix>;`
- `set var <name> <value>;`

## Changes

- Wire Go CLI variable commands to the admin API.
- Support integer and quoted string values in `SET VAR`.
- Return variable rows as `data_type`, `name`, `setting_type`, and
`value`.
- Add exact-name lookup with prefix fallback for `SHOW VAR`.
- Validate values by stored data type: `string`, `integer`, `bool`, and
`json`.
- Keep the legacy Python admin CLI/server behavior aligned.
- Update admin CLI docs and add focused tests.

## Verification

- `go test -count=1 ./internal/cli`
- `python3.12 -m py_compile admin/server/services.py
admin/server/routes.py api/db/services/system_settings_service.py
admin/client/parser.py admin/client/ragflow_client.py`
- Python admin CLI parser smoke test for `SET VAR`, quoted values, `SHOW
VAR`, and `LIST VARS`.
- Attempted `./run_go_tests.sh`; local environment is missing native
tokenizer/linker artifacts:
  - `internal/cpp/cmake-build-release/librag_tokenizer_c_api.a`
  - `-lstdc++`

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
2026-05-18 19:08:45 +08:00

236 lines
6.4 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cli
import (
"encoding/json"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
)
func TestParseAdminVariableCommands(t *testing.T) {
tests := []struct {
name string
input string
command string
varName string
varValue string
hasValue bool
adminMode bool
}{
{
name: "list variables",
input: "list vars;",
command: "list_variables",
adminMode: true,
},
{
name: "show variables by prefix",
input: "show var mail;",
command: "show_variable",
varName: "mail",
adminMode: true,
},
{
name: "set integer variable",
input: "set var mail.port 15;",
command: "set_variable",
varName: "mail.port",
varValue: "15",
hasValue: true,
adminMode: true,
},
{
name: "set quoted string variable",
input: `set var mail.server "local host";`,
command: "set_variable",
varName: "mail.server",
varValue: "local host",
hasValue: true,
adminMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd, err := NewParser(tt.input).Parse(tt.adminMode)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if cmd.Type != tt.command {
t.Fatalf("command type = %q, want %q", cmd.Type, tt.command)
}
if tt.varName != "" && cmd.Params["var_name"] != tt.varName {
t.Fatalf("var_name = %v, want %q", cmd.Params["var_name"], tt.varName)
}
if tt.hasValue && cmd.Params["var_value"] != tt.varValue {
t.Fatalf("var_value = %v, want %q", cmd.Params["var_value"], tt.varValue)
}
})
}
}
func newAdminTestClient(t *testing.T, handler http.HandlerFunc) (*RAGFlowClient, func()) {
t.Helper()
server := httptest.NewServer(handler)
serverURL, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("parse test server URL: %v", err)
}
host, portText, err := net.SplitHostPort(serverURL.Host)
if err != nil {
t.Fatalf("split host port: %v", err)
}
port, err := strconv.Atoi(portText)
if err != nil {
t.Fatalf("parse port: %v", err)
}
client := NewRAGFlowClient("admin")
client.HTTPClient.Host = host
client.HTTPClient.Port = port
client.HTTPClient.client = server.Client()
client.HTTPClient.LoginToken = "test-token"
return client, server.Close
}
func TestListVariablesUsesAdminVariablesEndpoint(t *testing.T) {
client, closeServer := newAdminTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("method = %s, want GET", r.Method)
return
}
if r.URL.Path != "/api/v1/admin/variables" {
t.Errorf("path = %s, want /api/v1/admin/variables", r.URL.Path)
return
}
if r.Header.Get("Authorization") != "test-token" {
t.Errorf("Authorization header = %q", r.Header.Get("Authorization"))
return
}
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"message": "",
"data": []map[string]interface{}{
{"data_type": "string", "name": "mail.server", "source": "variable", "value": "localhost"},
},
})
})
defer closeServer()
resp, err := client.ListVariables(NewCommand("list_variables"))
if err != nil {
t.Fatalf("ListVariables() error = %v", err)
}
result := resp.(*CommonResponse)
if got := result.Data[0]["setting_type"]; got != "config" {
t.Fatalf("setting_type = %v, want config", got)
}
if _, ok := result.Data[0]["source"]; ok {
t.Fatalf("source column should be normalized away: %#v", result.Data[0])
}
}
func TestShowVariableSendsRequestedName(t *testing.T) {
client, closeServer := newAdminTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("method = %s, want GET", r.Method)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("read body: %v", err)
return
}
var request map[string]string
if err := json.Unmarshal(body, &request); err != nil {
t.Errorf("request body is not JSON: %v", err)
return
}
if request["var_name"] != "mail" {
t.Errorf("var_name = %q, want mail", request["var_name"])
return
}
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"message": "",
"data": []map[string]interface{}{
{"data_type": "string", "name": "mail.server", "setting_type": "config", "value": "localhost"},
},
})
})
defer closeServer()
cmd := NewCommand("show_variable")
cmd.Params["var_name"] = "mail"
resp, err := client.ShowVariable(cmd)
if err != nil {
t.Fatalf("ShowVariable() error = %v", err)
}
result := resp.(*CommonResponse)
if got := result.Data[0]["name"]; got != "mail.server" {
t.Fatalf("name = %v, want mail.server", got)
}
}
func TestSetVariableReturnsServerConfirmation(t *testing.T) {
client, closeServer := newAdminTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("method = %s, want PUT", r.Method)
return
}
var request map[string]string
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Errorf("request body is not JSON: %v", err)
return
}
if request["var_name"] != "mail.server" {
t.Errorf("var_name = %q, want mail.server", request["var_name"])
return
}
if request["var_value"] != "localhost" {
t.Errorf("var_value = %q, want localhost", request["var_value"])
return
}
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"message": "Set variable successfully",
"data": nil,
})
})
defer closeServer()
cmd := NewCommand("set_variable")
cmd.Params["var_name"] = "mail.server"
cmd.Params["var_value"] = "localhost"
resp, err := client.SetVariable(cmd)
if err != nil {
t.Fatalf("SetVariable() error = %v", err)
}
result := resp.(*MessageResponse)
if result.Message != "Set variable successfully" {
t.Fatalf("message = %q, want Set variable successfully", result.Message)
}
}