615 lines
20 KiB
Go
615 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"gocoder/agents"
|
|
"gocoder/db"
|
|
)
|
|
|
|
func TestResolveWorkspaceGroupIDPrefersConversationGroup(t *testing.T) {
|
|
convGroupID := "conversation-group"
|
|
conv := &db.Conversation{GroupID: &convGroupID}
|
|
|
|
got := resolveWorkspaceGroupID(conv, "request-group")
|
|
if got != convGroupID {
|
|
t.Fatalf("resolveWorkspaceGroupID returned %q, want %q", got, convGroupID)
|
|
}
|
|
}
|
|
|
|
func TestResolveWorkspaceGroupIDFallsBackToRequestGroup(t *testing.T) {
|
|
got := resolveWorkspaceGroupID(nil, "request-group")
|
|
if got != "request-group" {
|
|
t.Fatalf("resolveWorkspaceGroupID returned %q, want %q", got, "request-group")
|
|
}
|
|
}
|
|
|
|
func TestResolveWorkspaceGroupIDReturnsEmptyWhenUnset(t *testing.T) {
|
|
conv := &db.Conversation{}
|
|
|
|
got := resolveWorkspaceGroupID(conv, "")
|
|
if got != "" {
|
|
t.Fatalf("resolveWorkspaceGroupID returned %q, want empty string", got)
|
|
}
|
|
}
|
|
|
|
func TestFriendlyToolDisplayName(t *testing.T) {
|
|
cases := map[string]string{
|
|
"planner_agent": "Planner",
|
|
"programmer_agent": "Programmer",
|
|
"qa_agent": "QA",
|
|
"write_file": "write_file",
|
|
}
|
|
|
|
for input, want := range cases {
|
|
if got := friendlyToolDisplayName(input); got != want {
|
|
t.Fatalf("friendlyToolDisplayName(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStageStatusKind(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Planner": "planner",
|
|
"Programmer": "programmer",
|
|
"QA": "qa",
|
|
"Manager": "manager",
|
|
"Other": "thinking",
|
|
}
|
|
|
|
for input, want := range cases {
|
|
if got := stageStatusKind(input); got != want {
|
|
t.Fatalf("stageStatusKind(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSubagentNoResponseError(t *testing.T) {
|
|
got := subagentNoResponseError("QA", "it kept calling tools but never produced a final answer before hitting the 20-loop limit")
|
|
want := "QA returned no response: it kept calling tools but never produced a final answer before hitting the 20-loop limit"
|
|
if got != want {
|
|
t.Fatalf("subagentNoResponseError returned %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSubagentNoResponseErrorDefaultsStageName(t *testing.T) {
|
|
got := subagentNoResponseError(" ", " ")
|
|
want := "Subagent returned no response"
|
|
if got != want {
|
|
t.Fatalf("subagentNoResponseError returned %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCollectThinkingAndContentPrefersExplicitReasoning(t *testing.T) {
|
|
thinking, content := collectThinkingAndContent(" plan ", "", "final answer")
|
|
if got, want := thinking, "plan"; got != want {
|
|
t.Fatalf("thinking = %q, want %q", got, want)
|
|
}
|
|
if got, want := content, "final answer"; got != want {
|
|
t.Fatalf("content = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSplitThinkTagsFromContentSeparatesThinkingFromAnswer(t *testing.T) {
|
|
thinking, content := splitThinkTagsFromContent("before <think>inner</think> after")
|
|
if got, want := thinking, "inner"; got != want {
|
|
t.Fatalf("thinking = %q, want %q", got, want)
|
|
}
|
|
if got, want := content, "before after"; got != want {
|
|
t.Fatalf("content = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestFinalizeStreamedToolCallsOrdersAndConcatenatesArguments(t *testing.T) {
|
|
toolCalls := map[int]*streamedToolCall{}
|
|
appendStreamedToolCall(toolCalls, LMStudioStreamToolCallDelta{
|
|
Index: 1,
|
|
ID: "call_2",
|
|
Type: "function",
|
|
Function: LMStudioStreamToolCallFunctionDelta{
|
|
Name: "qa_agent",
|
|
Arguments: `{"task":"Check`,
|
|
},
|
|
})
|
|
appendStreamedToolCall(toolCalls, LMStudioStreamToolCallDelta{
|
|
Index: 0,
|
|
ID: "call_1",
|
|
Type: "function",
|
|
Function: LMStudioStreamToolCallFunctionDelta{
|
|
Name: "planner_agent",
|
|
Arguments: `{"task":"Plan"}`,
|
|
},
|
|
})
|
|
appendStreamedToolCall(toolCalls, LMStudioStreamToolCallDelta{
|
|
Index: 1,
|
|
Function: LMStudioStreamToolCallFunctionDelta{
|
|
Arguments: ` the result"}`,
|
|
},
|
|
})
|
|
|
|
got := finalizeStreamedToolCalls(toolCalls)
|
|
if len(got) != 2 {
|
|
t.Fatalf("finalizeStreamedToolCalls returned %d calls, want 2", len(got))
|
|
}
|
|
if got[0].ID != "call_1" || got[0].Function.Name != "planner_agent" {
|
|
t.Fatalf("first tool call = %+v, want planner_agent call_1", got[0])
|
|
}
|
|
if string(got[0].Function.Arguments) != `{"task":"Plan"}` {
|
|
t.Fatalf("first tool call args = %s, want %s", string(got[0].Function.Arguments), `{"task":"Plan"}`)
|
|
}
|
|
if got[1].ID != "call_2" || got[1].Function.Name != "qa_agent" {
|
|
t.Fatalf("second tool call = %+v, want qa_agent call_2", got[1])
|
|
}
|
|
if string(got[1].Function.Arguments) != `{"task":"Check the result"}` {
|
|
t.Fatalf("second tool call args = %s, want %s", string(got[1].Function.Arguments), `{"task":"Check the result"}`)
|
|
}
|
|
}
|
|
|
|
func TestAPIMessagesKeepEmptyContentFields(t *testing.T) {
|
|
req := apiRequest{
|
|
Model: "test-model",
|
|
Messages: []apiMessagePart{
|
|
{
|
|
Role: "assistant",
|
|
Content: "",
|
|
ToolCalls: []ToolCallAI{
|
|
{
|
|
ID: "call_1",
|
|
Type: "function",
|
|
Function: ToolCallFunction{
|
|
Name: "planner_agent",
|
|
Arguments: ToolCallArguments(`{"task":"Plan"}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Role: "tool",
|
|
Content: "",
|
|
ToolCallID: "call_1",
|
|
Name: "planner_agent",
|
|
},
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(req)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal returned error: %v", err)
|
|
}
|
|
|
|
var decoded struct {
|
|
Messages []struct {
|
|
Content string `json:"content"`
|
|
ToolCalls []struct {
|
|
Function struct {
|
|
Arguments string `json:"arguments"`
|
|
} `json:"function"`
|
|
} `json:"tool_calls"`
|
|
} `json:"messages"`
|
|
}
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
|
}
|
|
if len(decoded.Messages) != 2 {
|
|
t.Fatalf("decoded %d messages, want 2", len(decoded.Messages))
|
|
}
|
|
if got, want := decoded.Messages[0].Content, ""; got != want {
|
|
t.Fatalf("assistant message content = %q, want %q", got, want)
|
|
}
|
|
if len(decoded.Messages[0].ToolCalls) != 1 {
|
|
t.Fatalf("assistant message tool call count = %d, want 1", len(decoded.Messages[0].ToolCalls))
|
|
}
|
|
if got, want := decoded.Messages[0].ToolCalls[0].Function.Arguments, `{"task":"Plan"}`; got != want {
|
|
t.Fatalf("assistant tool call arguments = %q, want %q", got, want)
|
|
}
|
|
if got, want := decoded.Messages[1].Content, ""; got != want {
|
|
t.Fatalf("tool message content = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildPromptHistoryInjectsSummaryAndSkipsEarlierMessages(t *testing.T) {
|
|
history := []db.Message{
|
|
{Order: 1, Role: "user", Kind: "message", Content: "old user"},
|
|
{Order: 2, Role: "assistant", Kind: "message", Content: "old assistant"},
|
|
{Order: 3, Role: "user", Kind: "message", Content: "new user"},
|
|
}
|
|
|
|
got := buildPromptHistory(history, "compressed memory", 2)
|
|
if len(got) != 2 {
|
|
t.Fatalf("buildPromptHistory returned %d messages, want 2", len(got))
|
|
}
|
|
if got[0].Role != "system" || got[0].Content == "" {
|
|
t.Fatalf("buildPromptHistory did not inject a system summary message: %+v", got[0])
|
|
}
|
|
if got[1].Content != "new user" {
|
|
t.Fatalf("buildPromptHistory kept wrong history tail: %+v", got[1])
|
|
}
|
|
}
|
|
|
|
func TestAgentRegistryIncludesCompactor(t *testing.T) {
|
|
registry := agents.NewAgentRegistry()
|
|
compactor := registry.GetByType("compactor")
|
|
if compactor == nil {
|
|
t.Fatal("expected compactor agent to be registered")
|
|
}
|
|
if got, want := compactor.DisplayName(), "Compactor"; got != want {
|
|
t.Fatalf("compactor display name = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestConfiguredCompactionRatiosDefaultToSafeValues(t *testing.T) {
|
|
originalTrigger := config.LLM.ContextCompactionTriggerRatio
|
|
originalTarget := config.LLM.ContextCompactionTargetRatio
|
|
t.Cleanup(func() {
|
|
config.LLM.ContextCompactionTriggerRatio = originalTrigger
|
|
config.LLM.ContextCompactionTargetRatio = originalTarget
|
|
})
|
|
|
|
config.LLM.ContextCompactionTriggerRatio = 0
|
|
config.LLM.ContextCompactionTargetRatio = 0
|
|
|
|
if got, want := configuredCompactionTriggerRatio(), 0.80; got != want {
|
|
t.Fatalf("configuredCompactionTriggerRatio() = %v, want %v", got, want)
|
|
}
|
|
if got, want := configuredCompactionTargetRatio(), 0.70; got != want {
|
|
t.Fatalf("configuredCompactionTargetRatio() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDefaultConfigUsesBuiltInServerDefaults(t *testing.T) {
|
|
cfg := defaultConfig()
|
|
if got, want := cfg.Server.Addr, defaultServerAddr; got != want {
|
|
t.Fatalf("default server addr = %q, want %q", got, want)
|
|
}
|
|
if got, want := cfg.Server.Host, defaultServerHost; got != want {
|
|
t.Fatalf("default server host = %q, want %q", got, want)
|
|
}
|
|
if cfg.LLM.AutoOpenReasoning == nil || !*cfg.LLM.AutoOpenReasoning {
|
|
t.Fatalf("default auto_open_reasoning = %+v, want true", cfg.LLM.AutoOpenReasoning)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeConfigDefaultsAutoOpenReasoning(t *testing.T) {
|
|
cfg := Config{}
|
|
|
|
normalizeConfig(&cfg)
|
|
|
|
if cfg.LLM.AutoOpenReasoning == nil || !*cfg.LLM.AutoOpenReasoning {
|
|
t.Fatalf("normalizeConfig did not default auto_open_reasoning to true: %+v", cfg.LLM.AutoOpenReasoning)
|
|
}
|
|
}
|
|
|
|
func TestValidateContextCompactionSettingsRejectsInvalidValues(t *testing.T) {
|
|
cases := []contextCompactionSettings{
|
|
{ContextTokens: 0, ContextCompactionTriggerRatio: 0.8, ContextCompactionTargetRatio: 0.7},
|
|
{ContextTokens: 100, ContextCompactionTriggerRatio: 1, ContextCompactionTargetRatio: 0.7},
|
|
{ContextTokens: 100, ContextCompactionTriggerRatio: 0.8, ContextCompactionTargetRatio: 0.8},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
if err := validateContextCompactionSettings(tc); err == nil {
|
|
t.Fatalf("validateContextCompactionSettings(%+v) = nil, want error", tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateAppSettingsRejectsInvalidValues(t *testing.T) {
|
|
base := defaultConfig()
|
|
base.LLM.Provider = "openai"
|
|
base.LLM.BaseURL = "http://localhost:1234/v1"
|
|
base.LLM.Model = "qwen/qwen3.6-35b-a3b"
|
|
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*AppSettings)
|
|
}{
|
|
{name: "missing server addr", mutate: func(s *AppSettings) { s.ServerAddr = "" }},
|
|
{name: "missing server host", mutate: func(s *AppSettings) { s.ServerHost = "" }},
|
|
{name: "missing provider", mutate: func(s *AppSettings) { s.Provider = "" }},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
settings := AppSettings{
|
|
LLMConfig: base.LLM,
|
|
ServerAddr: base.Server.Addr,
|
|
ServerHost: base.Server.Host,
|
|
}
|
|
tc.mutate(&settings)
|
|
if err := validateAppSettings(settings); err == nil {
|
|
t.Fatalf("validateAppSettings(%s) = nil, want error", tc.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateLLMSettingsRejectsInvalidValues(t *testing.T) {
|
|
base := LLMConfig{
|
|
Provider: "openai",
|
|
BaseURL: "http://localhost:1234/v1",
|
|
APIKey: "lm-studio",
|
|
Model: "qwen/qwen3.6-35b-a3b",
|
|
ContextTokens: 1024,
|
|
ContextCompactionTriggerRatio: 0.8,
|
|
ContextCompactionTargetRatio: 0.7,
|
|
Temperature: 0.3,
|
|
MaxTokens: 1024,
|
|
TopP: 1.0,
|
|
FrequencyPenalty: 0,
|
|
PresencePenalty: 0,
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*LLMConfig)
|
|
}{
|
|
{name: "missing provider", mutate: func(cfg *LLMConfig) { cfg.Provider = "" }},
|
|
{name: "missing base url", mutate: func(cfg *LLMConfig) { cfg.BaseURL = "" }},
|
|
{name: "missing model", mutate: func(cfg *LLMConfig) { cfg.Model = "" }},
|
|
{name: "invalid max tokens", mutate: func(cfg *LLMConfig) { cfg.MaxTokens = 0 }},
|
|
{name: "invalid top p", mutate: func(cfg *LLMConfig) { cfg.TopP = 0 }},
|
|
{name: "invalid context tokens", mutate: func(cfg *LLMConfig) { cfg.ContextTokens = 0 }},
|
|
{name: "invalid trigger ratio", mutate: func(cfg *LLMConfig) { cfg.ContextCompactionTriggerRatio = 1 }},
|
|
{name: "invalid target ratio", mutate: func(cfg *LLMConfig) { cfg.ContextCompactionTargetRatio = 0.8 }},
|
|
{name: "invalid reasoning effort", mutate: func(cfg *LLMConfig) { cfg.ReasoningEffort = "ultra" }},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
cfg := base
|
|
tc.mutate(&cfg)
|
|
if err := validateLLMSettings(cfg); err == nil {
|
|
t.Fatalf("validateLLMSettings(%s) = nil, want error", tc.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateLLMSettingsAcceptsReasoningEffortValues(t *testing.T) {
|
|
base := LLMConfig{
|
|
Provider: "openai",
|
|
BaseURL: "http://localhost:1234/v1",
|
|
APIKey: "lm-studio",
|
|
Model: "qwen/qwen3.6-35b-a3b",
|
|
ContextTokens: 1024,
|
|
ContextCompactionTriggerRatio: 0.8,
|
|
ContextCompactionTargetRatio: 0.7,
|
|
Temperature: 0.3,
|
|
MaxTokens: 1024,
|
|
TopP: 1.0,
|
|
FrequencyPenalty: 0,
|
|
PresencePenalty: 0,
|
|
}
|
|
|
|
for _, effort := range []string{"", "low", "medium", "high", " HIGH "} {
|
|
cfg := base
|
|
cfg.ReasoningEffort = effort
|
|
if err := validateLLMSettings(cfg); err != nil {
|
|
t.Fatalf("validateLLMSettings(%q) returned error: %v", effort, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateContextCompactionSettingsPersistsToDisk(t *testing.T) {
|
|
original := currentConfig()
|
|
t.Cleanup(func() {
|
|
setConfig(original)
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
originalWD, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("os.Getwd returned error: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = os.Chdir(originalWD)
|
|
})
|
|
if err := os.Chdir(tempDir); err != nil {
|
|
t.Fatalf("os.Chdir returned error: %v", err)
|
|
}
|
|
|
|
configCopy := defaultConfig()
|
|
configCopy.Server.Addr = ":9999"
|
|
configCopy.Server.Host = "localhost"
|
|
configCopy.LLM.ContextTokens = 512
|
|
configCopy.LLM.ContextCompactionTriggerRatio = 0.8
|
|
configCopy.LLM.ContextCompactionTargetRatio = 0.7
|
|
setConfig(configCopy)
|
|
|
|
settings := contextCompactionSettings{
|
|
ContextTokens: 2048,
|
|
ContextCompactionTriggerRatio: 0.85,
|
|
ContextCompactionTargetRatio: 0.65,
|
|
}
|
|
if err := updateContextCompactionSettings(settings); err != nil {
|
|
t.Fatalf("updateContextCompactionSettings returned error: %v", err)
|
|
}
|
|
|
|
updated := currentConfig()
|
|
if got, want := updated.LLM.ContextTokens, settings.ContextTokens; got != want {
|
|
t.Fatalf("updated context tokens = %d, want %d", got, want)
|
|
}
|
|
if got, want := updated.LLM.ContextCompactionTriggerRatio, settings.ContextCompactionTriggerRatio; got != want {
|
|
t.Fatalf("updated trigger ratio = %v, want %v", got, want)
|
|
}
|
|
if got, want := updated.LLM.ContextCompactionTargetRatio, settings.ContextCompactionTargetRatio; got != want {
|
|
t.Fatalf("updated target ratio = %v, want %v", got, want)
|
|
}
|
|
if got, want := updated.Server.Addr, configCopy.Server.Addr; got != want {
|
|
t.Fatalf("updated server addr = %q, want %q", got, want)
|
|
}
|
|
if got, want := updated.Server.Host, configCopy.Server.Host; got != want {
|
|
t.Fatalf("updated server host = %q, want %q", got, want)
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(tempDir, "config.json"))
|
|
if err != nil {
|
|
t.Fatalf("os.ReadFile returned error: %v", err)
|
|
}
|
|
if len(data) == 0 {
|
|
t.Fatal("config.json was not written")
|
|
}
|
|
}
|
|
|
|
func TestUpdateLLMSettingsPersistsFullConfig(t *testing.T) {
|
|
original := currentConfig()
|
|
t.Cleanup(func() {
|
|
setConfig(original)
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
originalWD, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("os.Getwd returned error: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = os.Chdir(originalWD)
|
|
})
|
|
if err := os.Chdir(tempDir); err != nil {
|
|
t.Fatalf("os.Chdir returned error: %v", err)
|
|
}
|
|
|
|
configCopy := defaultConfig()
|
|
configCopy.Server.Addr = ":9999"
|
|
configCopy.Server.Host = "localhost"
|
|
configCopy.LLM = LLMConfig{
|
|
Provider: "openai",
|
|
BaseURL: "http://localhost:4321/v1",
|
|
APIKey: "new-key",
|
|
Model: "test-model",
|
|
ContextTokens: 4096,
|
|
ContextCompactionTriggerRatio: 0.85,
|
|
ContextCompactionTargetRatio: 0.6,
|
|
Temperature: 0.2,
|
|
MaxTokens: 2048,
|
|
TopP: 0.95,
|
|
FrequencyPenalty: 0.25,
|
|
PresencePenalty: 0.1,
|
|
}
|
|
setConfig(configCopy)
|
|
|
|
settings := LLMConfig{
|
|
Provider: "anthropic",
|
|
BaseURL: "https://example.com/v1",
|
|
APIKey: "updated-key",
|
|
Model: "claude",
|
|
ReasoningEffort: "high",
|
|
AutoOpenReasoning: boolPtr(false),
|
|
ContextTokens: 8192,
|
|
ContextCompactionTriggerRatio: 0.9,
|
|
ContextCompactionTargetRatio: 0.7,
|
|
Temperature: 0.15,
|
|
MaxTokens: 4096,
|
|
TopP: 0.9,
|
|
FrequencyPenalty: 0.4,
|
|
PresencePenalty: 0.2,
|
|
}
|
|
|
|
if err := updateLLMSettings(settings); err != nil {
|
|
t.Fatalf("updateLLMSettings returned error: %v", err)
|
|
}
|
|
|
|
updated := currentConfig()
|
|
if got, want := updated.LLM, settings; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("updated LLM config = %+v, want %+v", got, want)
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(tempDir, "config.json"))
|
|
if err != nil {
|
|
t.Fatalf("os.ReadFile returned error: %v", err)
|
|
}
|
|
|
|
var persisted Config
|
|
if err := json.Unmarshal(data, &persisted); err != nil {
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
|
}
|
|
if got, want := persisted.LLM, settings; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("persisted LLM config = %+v, want %+v", got, want)
|
|
}
|
|
if got, want := persisted.Server.Addr, configCopy.Server.Addr; got != want {
|
|
t.Fatalf("persisted server addr = %q, want %q", got, want)
|
|
}
|
|
if got, want := persisted.Server.Host, configCopy.Server.Host; got != want {
|
|
t.Fatalf("persisted server host = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestUpdateAppSettingsPersistsFullConfig(t *testing.T) {
|
|
original := currentConfig()
|
|
t.Cleanup(func() {
|
|
setConfig(original)
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
originalWD, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("os.Getwd returned error: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = os.Chdir(originalWD)
|
|
})
|
|
if err := os.Chdir(tempDir); err != nil {
|
|
t.Fatalf("os.Chdir returned error: %v", err)
|
|
}
|
|
|
|
configCopy := defaultConfig()
|
|
configCopy.Server.Addr = ":9999"
|
|
configCopy.Server.Host = "localhost"
|
|
setConfig(configCopy)
|
|
|
|
settings := AppSettings{
|
|
LLMConfig: LLMConfig{
|
|
Provider: "anthropic",
|
|
BaseURL: "https://example.com/v1",
|
|
APIKey: "updated-key",
|
|
Model: "claude",
|
|
ReasoningEffort: "low",
|
|
AutoOpenReasoning: boolPtr(false),
|
|
ContextTokens: 8192,
|
|
ContextCompactionTriggerRatio: 0.9,
|
|
ContextCompactionTargetRatio: 0.7,
|
|
Temperature: 0.15,
|
|
MaxTokens: 4096,
|
|
TopP: 0.9,
|
|
FrequencyPenalty: 0.4,
|
|
PresencePenalty: 0.2,
|
|
},
|
|
ServerAddr: ":9090",
|
|
ServerHost: "127.0.0.1",
|
|
}
|
|
|
|
if err := updateAppSettings(settings); err != nil {
|
|
t.Fatalf("updateAppSettings returned error: %v", err)
|
|
}
|
|
|
|
updated := currentConfig()
|
|
if got, want := updated.LLM, settings.LLMConfig; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("updated LLM config = %+v, want %+v", got, want)
|
|
}
|
|
if got, want := updated.Server.Addr, settings.ServerAddr; got != want {
|
|
t.Fatalf("updated server addr = %q, want %q", got, want)
|
|
}
|
|
if got, want := updated.Server.Host, settings.ServerHost; got != want {
|
|
t.Fatalf("updated server host = %q, want %q", got, want)
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(tempDir, "config.json"))
|
|
if err != nil {
|
|
t.Fatalf("os.ReadFile returned error: %v", err)
|
|
}
|
|
|
|
var persisted Config
|
|
if err := json.Unmarshal(data, &persisted); err != nil {
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
|
}
|
|
if got, want := persisted.LLM, settings.LLMConfig; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("persisted LLM config = %+v, want %+v", got, want)
|
|
}
|
|
if got, want := persisted.Server.Addr, settings.ServerAddr; got != want {
|
|
t.Fatalf("persisted server addr = %q, want %q", got, want)
|
|
}
|
|
if got, want := persisted.Server.Host, settings.ServerHost; got != want {
|
|
t.Fatalf("persisted server host = %q, want %q", got, want)
|
|
}
|
|
}
|