iop/apps/edge/internal/configrefresh/provider_classify_test.go
toki 01dc2ef78b refactor: readability baseline 및 테스트 구조 개선
- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동
- 새 테스트 파일 추가 (edge, node, client, config, readability)
- readability_audit 스크립트 및 baseline 추가
- roadmap/SDD 문서 갱신
- agent-client/pi/extensions/openai-sampling-parameters 추가
2026-07-17 16:02:12 +09:00

597 lines
17 KiB
Go

package configrefresh_test
import (
"context"
"testing"
"iop/apps/edge/internal/configrefresh"
)
// TestClassifyProviderCapacityApplied verifies S07: provider capacity change is classified as applied.
func TestClassifyProviderCapacityApplied(t *testing.T) {
dir := t.TempDir()
currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
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: 8
max_queue: 4
queue_timeout_ms: 5000
`
candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)
current := buildNormalizedCurrent(t, currentPath)
ctx := context.Background()
result, _, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
Mode: configrefresh.ModeDryRun,
ConfigPath: candidatePath,
RequestID: "test-s07",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusApplied {
t.Errorf("expected status=%q, got %q (summary: %s)", configrefresh.StatusApplied, result.Status, result.Summary)
}
if len(result.Changes) == 0 {
t.Fatalf("expected at least one change")
}
found := false
for _, c := range result.Changes {
if c.Class == configrefresh.StatusApplied {
found = true
}
if c.Class == configrefresh.StatusRestartRequired {
t.Errorf("unexpected restart_required change: %s", c.Path)
}
}
if !found {
t.Errorf("no applied change found in: %+v", result.Changes)
}
}
func TestClassifyProviderStructuralChangeRestartRequired(t *testing.T) {
dir := t.TempDir()
currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
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: "vllm"
category: "api"
adapter: "cli"
models: ["llama3.1"]
capacity: 2
max_queue: 4
queue_timeout_ms: 5000
`
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-provider-structural",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusRestartRequired {
t.Fatalf("expected status=%q, got %q changes=%+v", configrefresh.StatusRestartRequired, result.Status, result.Changes)
}
found := false
for _, change := range result.Changes {
if change.Path == `nodes[].providers["prov-a"].type` && change.Class == configrefresh.StatusRestartRequired {
found = true
}
}
if !found {
t.Fatalf("provider structural type change not found in %+v", result.Changes)
}
}
// TestRefreshInvalidProviderRejected verifies S09: invalid provider config is classified as rejected.
func TestRefreshInvalidProviderRejected(t *testing.T) {
dir := t.TempDir()
currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
// Invalid: shared validator rejects enabled openai_compat without endpoint.
invalidProviderYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
openai_compat:
enabled: true
endpoint: ""
`
invalidPath := writeYAML(t, dir, "invalid_provider.yaml", invalidProviderYAML)
current := buildNormalizedCurrent(t, currentPath)
ctx := context.Background()
result, candidate, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
Mode: configrefresh.ModeDryRun,
ConfigPath: invalidPath,
RequestID: "test-s09-provider",
})
if err != nil {
t.Fatalf("Evaluate should not return error for rejected config: %v", err)
}
if result.Status != configrefresh.StatusRejected {
t.Errorf("expected status=%q, got %q (summary: %s)", configrefresh.StatusRejected, result.Status, result.Summary)
}
if candidate != nil {
t.Errorf("expected nil candidate for rejected result")
}
}
// TestClassifyProviderEnabledLiveApply verifies that toggling providers[].enabled
// is classified as StatusApplied (live apply, no restart required).
func TestClassifyProviderEnabledLiveApply(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
`
currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
// Candidate disables prov-a.
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
enabled: false
`
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-enabled-liveapply",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusApplied {
t.Fatalf("expected status=applied for enabled toggle, got %q", result.Status)
}
foundEnabled := false
for _, c := range result.Changes {
if c.Path == `nodes[].providers["prov-a"].enabled` {
foundEnabled = true
if c.Class != configrefresh.StatusApplied {
t.Errorf("providers[].enabled class: got %q, want applied", c.Class)
}
if c.Previous != "true" || c.Next != "false" {
t.Errorf("providers[].enabled change: prev=%q next=%q", c.Previous, c.Next)
}
}
if c.Class == configrefresh.StatusRestartRequired {
t.Errorf("unexpected restart_required change for enabled-only diff: %s", c.Path)
}
}
if !foundEnabled {
t.Errorf("providers[].enabled change path not found in: %+v", result.Changes)
}
}
// TestClassifyProviderFirstEndpointOnlyRestartRequired verifies that changing
// only the provider endpoint (type/category unchanged) is classified as
// restart_required and surfaces the correct path.
func TestClassifyProviderFirstEndpointOnlyRestartRequired(t *testing.T) {
dir := t.TempDir()
currentYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
providers:
- id: "prov-oc"
type: "openai_compat"
category: "api"
endpoint: "http://127.0.0.1:8000/v1"
models: ["model-a"]
capacity: 2
`
// Only endpoint changes — type, category, models, capacity are identical.
candidateYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
providers:
- id: "prov-oc"
type: "openai_compat"
category: "api"
endpoint: "http://127.0.0.1:9000/v1"
models: ["model-a"]
capacity: 2
`
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-endpoint-only",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusRestartRequired {
t.Fatalf("expected status=restart_required for endpoint-only change, got %q; changes=%+v", result.Status, result.Changes)
}
found := false
for _, c := range result.Changes {
if c.Path == `nodes[].providers["prov-oc"].endpoint` && c.Class == configrefresh.StatusRestartRequired {
found = true
}
}
if !found {
t.Errorf("expected restart_required change for providers[].endpoint; changes=%+v", result.Changes)
}
}
// TestClassifyProviderFirstHeadersChangeRestartRequired verifies that changing
// a provider headers map (type/category unchanged) is classified as
// restart_required — covers map deep-diff path.
func TestClassifyProviderFirstHeadersChangeRestartRequired(t *testing.T) {
dir := t.TempDir()
currentYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
providers:
- id: "prov-oc"
type: "openai_compat"
category: "api"
endpoint: "http://127.0.0.1:8000/v1"
headers:
Authorization: "Bearer old-token"
models: ["model-a"]
capacity: 2
`
// Only headers change.
candidateYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
providers:
- id: "prov-oc"
type: "openai_compat"
category: "api"
endpoint: "http://127.0.0.1:8000/v1"
headers:
Authorization: "Bearer new-token"
models: ["model-a"]
capacity: 2
`
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-headers-only",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusRestartRequired {
t.Fatalf("expected status=restart_required for headers change, got %q; changes=%+v", result.Status, result.Changes)
}
found := false
for _, c := range result.Changes {
if c.Path == `nodes[].providers["prov-oc"].headers` && c.Class == configrefresh.StatusRestartRequired {
found = true
}
}
if !found {
t.Errorf("expected restart_required change for providers[].headers; changes=%+v", result.Changes)
}
}
func TestClassifyProviderFirstExecutionFieldRestartRequired(t *testing.T) {
dir := t.TempDir()
currentYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
vllm:
enabled: true
endpoint: "http://127.0.0.1:8000/v1"
providers:
- id: "prov-vllm"
type: "vllm"
category: "api"
endpoint: "http://127.0.0.1:8000/v1"
models: ["model-a"]
capacity: 2
`
currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
// Candidate changes endpoint (provider-first execution field) → restart required.
candidateYAML := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-1"
alias: "n1"
token: "tok-1"
adapters:
vllm:
enabled: true
endpoint: "http://127.0.0.1:8080/v1"
providers:
- id: "prov-vllm"
type: "openai_compat"
category: "api"
endpoint: "http://127.0.0.1:8080/v1"
models: ["model-a"]
capacity: 2
`
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-provfirst-restart",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusRestartRequired {
t.Fatalf("expected status=restart_required for provider type change, got %q", result.Status)
}
hasTypeRestart := false
for _, c := range result.Changes {
if c.Path == `nodes[].providers["prov-vllm"].type` && c.Class == configrefresh.StatusRestartRequired {
hasTypeRestart = true
}
}
if !hasTypeRestart {
t.Errorf("expected restart_required for providers[].type change; changes: %+v", result.Changes)
}
}
// TestClassifyProviderPriorityApplied verifies that provider priority change
// is classified as StatusApplied (live apply, no restart required).
func TestClassifyProviderPriorityApplied(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
priority: 5
`
currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
// Candidate only changes priority.
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
priority: 1
`
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-priority-applied",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusApplied {
t.Fatalf("expected status=applied for priority-only change, got %q (summary: %s)", result.Status, result.Summary)
}
foundPriority := false
for _, c := range result.Changes {
if c.Path == `nodes[].providers["prov-a"].priority` {
foundPriority = true
if c.Class != configrefresh.StatusApplied {
t.Errorf("providers[].priority class: got %q, want applied", c.Class)
}
if c.Previous != "5" || c.Next != "1" {
t.Errorf("providers[].priority change: prev=%q next=%q", c.Previous, c.Next)
}
}
if c.Class == configrefresh.StatusRestartRequired {
t.Errorf("unexpected restart_required change for priority-only diff: %s", c.Path)
}
}
if !foundPriority {
t.Errorf("providers[].priority change path not found in: %+v", result.Changes)
}
}
// TestClassifyProviderLongContextCapacityApplied verifies that provider
// total_context_tokens and long_context_capacity changes are classified as
// live-apply (StatusApplied), consistent with capacity/priority.
func TestClassifyProviderLongContextCapacityApplied(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
total_context_tokens: 262144
long_context_capacity: 1
`
currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
// Candidate only changes the two long-context fields.
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
total_context_tokens: 524288
long_context_capacity: 2
`
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-capacity-applied",
})
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if result.Status != configrefresh.StatusApplied {
t.Fatalf("expected status=applied for long-context-only change, got %q (summary: %s)", result.Status, result.Summary)
}
foundTotal := false
foundLong := false
for _, c := range result.Changes {
switch c.Path {
case `nodes[].providers["prov-a"].total_context_tokens`:
foundTotal = true
if c.Class != configrefresh.StatusApplied {
t.Errorf("total_context_tokens class: got %q, want applied", c.Class)
}
if c.Previous != "262144" || c.Next != "524288" {
t.Errorf("total_context_tokens change: prev=%q next=%q", c.Previous, c.Next)
}
case `nodes[].providers["prov-a"].long_context_capacity`:
foundLong = true
if c.Class != configrefresh.StatusApplied {
t.Errorf("long_context_capacity class: got %q, want applied", c.Class)
}
if c.Previous != "1" || c.Next != "2" {
t.Errorf("long_context_capacity change: prev=%q next=%q", c.Previous, c.Next)
}
}
if c.Class == configrefresh.StatusRestartRequired {
t.Errorf("unexpected restart_required change for long-context-only diff: %s", c.Path)
}
}
if !foundTotal {
t.Errorf("providers[].total_context_tokens change path not found in: %+v", result.Changes)
}
if !foundLong {
t.Errorf("providers[].long_context_capacity change path not found in: %+v", result.Changes)
}
}