iop/apps/edge/internal/configrefresh/node_runtime_classify_test.go
toki f2306f4dc8 feat(protocol-profile): 멀티 프로토콜 프로필을 추가한다
OpenAI 호환 및 Anthropic Messages 경로의 프로토콜 프로필, 라우팅 계약, 테스트와 실행 증거를 함께 반영한다.
2026-08-01 10:33:10 +09:00

259 lines
6.9 KiB
Go

package configrefresh_test
import (
"context"
"testing"
"iop/apps/edge/internal/configrefresh"
"iop/packages/go/config"
)
// TestClassifyNodeRuntimeConcurrencyApplied verifies S15: node runtime
// concurrency is applied live.
func TestClassifyNodeRuntimeConcurrencyApplied(t *testing.T) {
dir := t.TempDir()
currentYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
cli:
enabled: true
runtime:
concurrency: 2
`
currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
concurrencyOnlyYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
cli:
enabled: true
runtime:
concurrency: 4
`
concurrencyOnlyPath := writeYAML(t, dir, "concurrency.yaml", concurrencyOnlyYAML)
current := buildNormalizedCurrent(t, currentPath)
ctx := context.Background()
// concurrency-only change → applied.
result, _, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
Mode: configrefresh.ModeDryRun,
ConfigPath: concurrencyOnlyPath,
RequestID: "test-s15-conc",
})
if err != nil {
t.Fatalf("Evaluate concurrency: %v", err)
}
if result.Status != configrefresh.StatusApplied {
t.Fatalf("expected concurrency change status=applied, got %q (summary: %s)", result.Status, result.Summary)
}
foundConc := false
for _, c := range result.Changes {
if c.Path == `nodes["node-1"].runtime.concurrency` {
foundConc = true
if c.Class != configrefresh.StatusApplied {
t.Errorf("expected concurrency class=applied, got %q", c.Class)
}
}
if c.Class == configrefresh.StatusRestartRequired {
t.Errorf("unexpected restart_required change for concurrency-only diff: %s", c.Path)
}
}
if !foundConc {
t.Errorf("concurrency change path not found in: %+v", result.Changes)
}
}
func TestClassifyModelTokenCounterApplied(t *testing.T) {
current := &config.EdgeConfig{Models: []config.ModelCatalogEntry{{
ID: "model-a", TokenCounter: &config.TokenCounterConf{Mode: config.TokenCounterDeterministic},
}}}
candidate := &config.EdgeConfig{Models: []config.ModelCatalogEntry{{
ID: "model-a", TokenCounter: &config.TokenCounterConf{Mode: config.TokenCounterEstimate, Per1kInput: 250},
}}}
result := configrefresh.Classify(current, candidate)
if result.Status != configrefresh.StatusApplied {
t.Fatalf("status = %q, want applied; changes=%+v", result.Status, result.Changes)
}
wantPath := `models["model-a"].token_counter`
for _, change := range result.Changes {
if change.Path == wantPath && change.Class == configrefresh.StatusApplied {
return
}
}
t.Fatalf("token_counter applied path missing: %+v", result.Changes)
}
func TestClassifyModelCatalogProviderMappingApplied(t *testing.T) {
dir := t.TempDir()
currentYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
cli:
enabled: true
providers:
- id: "prov-a"
type: "ollama"
category: "local_inference"
adapter: "cli"
models: ["llama3.1"]
capacity: 2
- id: "prov-b"
type: "ollama"
category: "local_inference"
adapter: "cli"
models: ["llama3.1"]
capacity: 2
models:
- id: "qwen3.6:35b"
display_name: "Qwen"
context_window_tokens: 131072
default_max_tokens: 8192
min_max_tokens: 8192
providers:
prov-a: "llama3.1"
`
candidateYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
cli:
enabled: true
providers:
- id: "prov-a"
type: "ollama"
category: "local_inference"
adapter: "cli"
models: ["llama3.1"]
capacity: 2
- id: "prov-b"
type: "ollama"
category: "local_inference"
adapter: "cli"
models: ["llama3.1"]
capacity: 2
models:
- id: "qwen3.6:35b"
display_name: "Qwen"
context_window_tokens: 262144
default_max_tokens: 32768
min_max_tokens: 32768
default_thinking_token_budget: 8192
providers:
prov-b: "llama3.1"
`
currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)
current := buildNormalizedCurrent(t, currentPath)
result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
Mode: configrefresh.ModeDryRun,
ConfigPath: candidatePath,
RequestID: "test-model-catalog",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusApplied {
t.Fatalf("expected status=%q, got %q changes=%+v", configrefresh.StatusApplied, result.Status, result.Changes)
}
found := map[string]bool{}
for _, change := range result.Changes {
if change.Class == configrefresh.StatusApplied {
found[change.Path] = true
}
}
for _, path := range []string{
`models["qwen3.6:35b"].context_window_tokens`,
`models["qwen3.6:35b"].default_max_tokens`,
`models["qwen3.6:35b"].min_max_tokens`,
`models["qwen3.6:35b"].default_thinking_token_budget`,
`models["qwen3.6:35b"].providers`,
} {
if !found[path] {
t.Fatalf("model catalog change %s not found in %+v", path, result.Changes)
}
}
}
func TestClassifyLongContextThresholdApplied(t *testing.T) {
dir := t.TempDir()
currentYAML := `
server:
listen: "0.0.0.0:9090"
long_context_threshold_tokens: 100000
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
cli:
enabled: true
`
candidateYAML := `
server:
listen: "0.0.0.0:9090"
long_context_threshold_tokens: 120000
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
cli:
enabled: true
`
currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)
current := buildNormalizedCurrent(t, currentPath)
result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
Mode: configrefresh.ModeDryRun,
ConfigPath: candidatePath,
RequestID: "test-long-threshold",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusApplied {
t.Fatalf("expected status=%q, got %q changes=%+v", configrefresh.StatusApplied, result.Status, result.Changes)
}
found := false
for _, change := range result.Changes {
if change.Path == "long_context_threshold_tokens" {
found = true
if change.Class != configrefresh.StatusApplied {
t.Errorf("threshold class: got %q, want applied", change.Class)
}
if change.Previous != "100000" || change.Next != "120000" {
t.Errorf("threshold diff: got %s→%s", change.Previous, change.Next)
}
}
if change.Class == configrefresh.StatusRestartRequired {
t.Errorf("unexpected restart_required threshold change: %s", change.Path)
}
}
if !found {
t.Fatalf("long_context_threshold_tokens change not found in %+v", result.Changes)
}
}