- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동 - 새 테스트 파일 추가 (edge, node, client, config, readability) - readability_audit 스크립트 및 baseline 추가 - roadmap/SDD 문서 갱신 - agent-client/pi/extensions/openai-sampling-parameters 추가
192 lines
4.6 KiB
Go
192 lines
4.6 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
func newTestConfig() *config.EdgeConfig {
|
|
return &config.EdgeConfig{
|
|
Server: config.EdgeServerConf{Listen: "127.0.0.1:0"},
|
|
Logging: config.LoggingConf{Level: "error"},
|
|
Metrics: config.MetricsConf{Port: 0},
|
|
Nodes: []config.NodeDefinition{
|
|
{ID: "node-1", Alias: "alias-1", Token: "tok-1"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func serveNormalizedLogPath() string {
|
|
if exe, err := os.Executable(); err == nil {
|
|
return filepath.Join(filepath.Dir(exe), "logs", "edge.log")
|
|
}
|
|
if wd, err := os.Getwd(); err == nil {
|
|
return filepath.Join(wd, "logs", "edge.log")
|
|
}
|
|
return "logs/edge.log"
|
|
}
|
|
|
|
func serveNormalizedArtifactDir(dir string) string {
|
|
if dir == "" {
|
|
dir = "artifacts"
|
|
}
|
|
if filepath.IsAbs(dir) {
|
|
return dir
|
|
}
|
|
bd := binaryDirForTest()
|
|
return filepath.Join(bd, dir)
|
|
}
|
|
|
|
func binaryDirForTest() string {
|
|
if exe, err := os.Executable(); err == nil {
|
|
return filepath.Dir(exe)
|
|
}
|
|
if wd, err := os.Getwd(); err == nil {
|
|
return wd
|
|
}
|
|
return "."
|
|
}
|
|
|
|
// loadServeNormalizedConfig loads an Edge config and applies the same
|
|
// normalization iop-edge serve performs, so refresh classification compares
|
|
// effective values.
|
|
func loadServeNormalizedConfig(t *testing.T, path string) *config.EdgeConfig {
|
|
t.Helper()
|
|
cfg, err := config.LoadEdge(path)
|
|
if err != nil {
|
|
t.Fatalf("load config %s: %v", path, err)
|
|
}
|
|
cfg.Logging.Path = serveNormalizedLogPath()
|
|
cfg.Bootstrap.ArtifactDir = serveNormalizedArtifactDir(cfg.Bootstrap.ArtifactDir)
|
|
return cfg
|
|
}
|
|
|
|
// providerPoolConfigYAML builds an Edge config with one provider-pool model and
|
|
// one node provider. The extra parameter lets callers inject a candidate
|
|
// variation (e.g. a different capacity or an invalid adapter block).
|
|
func providerPoolConfigYAML(serverAddr, openAIAddr, providerBlock string) string {
|
|
return fmt.Sprintf(`
|
|
server:
|
|
listen: %q
|
|
bootstrap:
|
|
listen: "0.0.0.0:18080"
|
|
artifact_dir: "artifacts"
|
|
logging:
|
|
level: "error"
|
|
refresh:
|
|
enabled: true
|
|
listen: "127.0.0.1:0"
|
|
openai:
|
|
enabled: true
|
|
listen: %q
|
|
adapter: "openai_compat"
|
|
target: ""
|
|
a2a:
|
|
listen: "0.0.0.0:8081"
|
|
metrics:
|
|
port: 0
|
|
models:
|
|
- id: "qwen3.6:35b"
|
|
display_name: "Qwen"
|
|
providers:
|
|
prov-a: "served-qwen"
|
|
nodes:
|
|
- id: "node-1"
|
|
alias: "n1"
|
|
token: "tok-1"
|
|
adapters:
|
|
%s
|
|
providers:
|
|
- id: "prov-a"
|
|
type: "vllm"
|
|
category: "api"
|
|
adapter: "vllm-gpu"
|
|
models: ["served-qwen"]
|
|
health: "available"
|
|
capacity: 2
|
|
max_queue: 4
|
|
queue_timeout_ms: 5000
|
|
`, serverAddr, openAIAddr, providerBlock)
|
|
}
|
|
|
|
const validAdapterBlock = ` openai_compat_instances:
|
|
- name: "vllm-gpu"
|
|
enabled: true
|
|
provider: "vllm"
|
|
endpoint: "http://127.0.0.1:8000/v1"`
|
|
|
|
// invalidAdapterBlock enables an openai_compat instance but leaves endpoint
|
|
// empty, which ValidateEdgeConfig rejects.
|
|
const invalidAdapterBlock = ` openai_compat_instances:
|
|
- name: "vllm-gpu"
|
|
enabled: true
|
|
provider: "vllm"
|
|
endpoint: ""`
|
|
|
|
// modelsResponse represents the JSON response from /v1/models.
|
|
type modelsResponse struct {
|
|
Object string `json:"object"`
|
|
Data []modelItem `json:"data"`
|
|
}
|
|
|
|
// modelItem represents a single model entry in the /v1/models response.
|
|
type modelItem struct {
|
|
ID string `json:"id"`
|
|
Object string `json:"object"`
|
|
Created int64 `json:"created"`
|
|
OwnedBy string `json:"owned_by"`
|
|
}
|
|
|
|
// modelIDs returns the sorted list of model IDs from /v1/models.
|
|
// This avoids flaky timestamp comparisons on the `created` field.
|
|
func modelIDs(t *testing.T, client *http.Client, openAIAddr string) []string {
|
|
t.Helper()
|
|
resp, err := client.Get("http://" + openAIAddr + "/v1/models")
|
|
if err != nil {
|
|
t.Fatalf("GET /v1/models: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("GET /v1/models status=%d", resp.StatusCode)
|
|
}
|
|
var body modelsResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode /v1/models body: %v", err)
|
|
}
|
|
ids := make([]string, 0, len(body.Data))
|
|
for _, m := range body.Data {
|
|
ids = append(ids, m.ID)
|
|
}
|
|
sort.Strings(ids)
|
|
return ids
|
|
}
|
|
|
|
func containsString(s []string, want string) bool {
|
|
for _, v := range s {
|
|
if v == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func freeTCPAddr(t *testing.T) string {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen free tcp addr: %v", err)
|
|
}
|
|
addr := ln.Addr().String()
|
|
if err := ln.Close(); err != nil {
|
|
t.Fatalf("close free tcp addr listener: %v", err)
|
|
}
|
|
return addr
|
|
}
|