Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
832
backend/internal/watcher/synthesizer/file_test.go
Normal file
832
backend/internal/watcher/synthesizer/file_test.go
Normal file
|
|
@ -0,0 +1,832 @@
|
|||
package synthesizer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
|
||||
)
|
||||
|
||||
func TestNewFileSynthesizer(t *testing.T) {
|
||||
synth := NewFileSynthesizer()
|
||||
if synth == nil {
|
||||
t.Fatal("expected non-nil synthesizer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_NilContext(t *testing.T) {
|
||||
synth := NewFileSynthesizer()
|
||||
auths, err := synth.Synthesize(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 0 {
|
||||
t.Fatalf("expected empty auths, got %d", len(auths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_EmptyAuthDir(t *testing.T) {
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: "",
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 0 {
|
||||
t.Fatalf("expected empty auths, got %d", len(auths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_NonExistentDir(t *testing.T) {
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: "/non/existent/path",
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 0 {
|
||||
t.Fatalf("expected empty auths, got %d", len(auths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_ValidAuthFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create a valid auth file
|
||||
authData := map[string]any{
|
||||
"type": "claude",
|
||||
"email": "test@example.com",
|
||||
"proxy_url": "http://proxy.local",
|
||||
"prefix": "test-prefix",
|
||||
"headers": map[string]string{
|
||||
" X-Test ": " value ",
|
||||
"X-Empty": " ",
|
||||
},
|
||||
"disable_cooling": true,
|
||||
"request_retry": 2,
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
err := os.WriteFile(filepath.Join(tempDir, "claude-auth.json"), data, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write auth file: %v", err)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
|
||||
if auths[0].Provider != "claude" {
|
||||
t.Errorf("expected provider claude, got %s", auths[0].Provider)
|
||||
}
|
||||
if auths[0].Label != "test@example.com" {
|
||||
t.Errorf("expected label test@example.com, got %s", auths[0].Label)
|
||||
}
|
||||
if auths[0].Prefix != "test-prefix" {
|
||||
t.Errorf("expected prefix test-prefix, got %s", auths[0].Prefix)
|
||||
}
|
||||
if auths[0].ProxyURL != "http://proxy.local" {
|
||||
t.Errorf("expected proxy_url http://proxy.local, got %s", auths[0].ProxyURL)
|
||||
}
|
||||
if got := auths[0].Attributes["header:X-Test"]; got != "value" {
|
||||
t.Errorf("expected header:X-Test value, got %q", got)
|
||||
}
|
||||
if _, ok := auths[0].Attributes["header:X-Empty"]; ok {
|
||||
t.Errorf("expected header:X-Empty to be absent, got %q", auths[0].Attributes["header:X-Empty"])
|
||||
}
|
||||
if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v {
|
||||
t.Errorf("expected disable_cooling true, got %v", auths[0].Metadata["disable_cooling"])
|
||||
}
|
||||
if v, ok := auths[0].Metadata["request_retry"].(float64); !ok || int(v) != 2 {
|
||||
t.Errorf("expected request_retry 2, got %v", auths[0].Metadata["request_retry"])
|
||||
}
|
||||
if auths[0].Status != coreauth.StatusActive {
|
||||
t.Errorf("expected status active, got %s", auths[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_LegacyKimiFingerprintProfile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
authData := map[string]any{
|
||||
"type": "kimi",
|
||||
"access_token": "kimi-access-token",
|
||||
"refresh_token": "kimi-refresh-token",
|
||||
"fingerprint-profile": "claude-code-cli",
|
||||
}
|
||||
data, errMarshal := json.Marshal(authData)
|
||||
if errMarshal != nil {
|
||||
t.Fatalf("marshal kimi auth: %v", errMarshal)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "kimi-auth.json"), data, 0644); err != nil {
|
||||
t.Fatalf("failed to write kimi auth file: %v", err)
|
||||
}
|
||||
|
||||
auths, err := NewFileSynthesizer().Synthesize(&SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
if auths[0].Provider != "kimi" {
|
||||
t.Fatalf("provider = %q, want kimi", auths[0].Provider)
|
||||
}
|
||||
if got := auths[0].Attributes["fingerprint_profile"]; got != "claude-code-cli" {
|
||||
t.Fatalf("attributes fingerprint_profile = %q, want claude-code-cli", got)
|
||||
}
|
||||
if got, _ := auths[0].Metadata["fingerprint_profile"].(string); got != "claude-code-cli" {
|
||||
t.Fatalf("metadata fingerprint_profile = %q, want claude-code-cli", got)
|
||||
}
|
||||
if _, exists := auths[0].Metadata["fingerprint-profile"]; exists {
|
||||
t.Fatalf("legacy fingerprint-profile was not normalized: %#v", auths[0].Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_IgnoresGeminiProviderFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authData := map[string]any{
|
||||
"type": "gemini",
|
||||
"email": "gemini@example.com",
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
err := os.WriteFile(filepath.Join(tempDir, "gemini-auth.json"), data, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write auth file: %v", err)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 0 {
|
||||
t.Fatalf("expected Gemini auth file to be ignored, got %d auths", len(auths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizeAuthFileExpandsPluginMultiAuths(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fullPath := filepath.Join(tempDir, "geminicli.json")
|
||||
raw := []byte(`{"type":"gemini-cli","excluded_models":["model-a"],"headers":{"X-Test":"value"}}`)
|
||||
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
|
||||
PluginAuthParser: multiAuthParserFunc(func(ctx context.Context, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
|
||||
if req.Provider != "gemini-cli" || req.Path != fullPath || req.FileName != "geminicli.json" {
|
||||
t.Fatalf("ParseAuths request = %#v, want file context", req)
|
||||
}
|
||||
return []*coreauth.Auth{
|
||||
{
|
||||
ID: "geminicli.json",
|
||||
Provider: "gemini-cli",
|
||||
Metadata: map[string]any{
|
||||
"type": "gemini-cli",
|
||||
"headers": map[string]any{
|
||||
"X-Test": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
{
|
||||
ID: "geminicli-project-a.json",
|
||||
Provider: "gemini-cli",
|
||||
Metadata: map[string]any{
|
||||
"type": "gemini-cli",
|
||||
"project_id": "project-a",
|
||||
"headers": map[string]any{
|
||||
"X-Test": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, true, nil
|
||||
}),
|
||||
}
|
||||
|
||||
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 2 {
|
||||
t.Fatalf("SynthesizeAuthFile() len = %d, want two plugin auths", len(auths))
|
||||
}
|
||||
if firstIndex, secondIndex := auths[0].EnsureIndex(), auths[1].EnsureIndex(); firstIndex == "" || firstIndex == secondIndex {
|
||||
t.Fatalf("auth indexes = %q/%q, want distinct non-empty indexes", firstIndex, secondIndex)
|
||||
}
|
||||
for _, auth := range auths {
|
||||
if !coreauth.IsPluginVirtualAuth(auth) {
|
||||
t.Fatalf("auth attributes = %#v, want plugin virtual marker", auth.Attributes)
|
||||
}
|
||||
if auth.Attributes[coreauth.AttributeVirtualSource] != fullPath {
|
||||
t.Fatalf("virtual_source = %q, want %q", auth.Attributes[coreauth.AttributeVirtualSource], fullPath)
|
||||
}
|
||||
if auth.Attributes["path"] != fullPath || auth.Attributes["source"] != fullPath {
|
||||
t.Fatalf("auth attributes = %#v, want source path", auth.Attributes)
|
||||
}
|
||||
if gotHeader := auth.Attributes["header:X-Test"]; gotHeader != "value" {
|
||||
t.Fatalf("header:X-Test = %q, want value", gotHeader)
|
||||
}
|
||||
if gotKind := auth.Attributes["auth_kind"]; gotKind != "oauth" {
|
||||
t.Fatalf("auth_kind = %q, want oauth", gotKind)
|
||||
}
|
||||
}
|
||||
if gotProject := auths[1].Metadata["project_id"]; gotProject != "project-a" {
|
||||
t.Fatalf("project_id = %#v, want project-a", gotProject)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizeAuthFileSkipsInvalidPluginAuthWeight(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fullPath := filepath.Join(tempDir, "plugin.json")
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
|
||||
PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
|
||||
return []*coreauth.Auth{
|
||||
{ID: "invalid", Provider: "plugin", Attributes: map[string]string{coreauth.AttributeWeight: "1.5"}},
|
||||
{ID: "valid", Provider: "plugin", Attributes: map[string]string{coreauth.AttributeWeight: "0"}},
|
||||
}, true, nil
|
||||
}),
|
||||
}
|
||||
|
||||
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, []byte(`{"type":"plugin"}`))
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 1 || auths[0].ID != "valid" {
|
||||
t.Fatalf("SynthesizeAuthFile() auths = %#v, want only valid zero-weight auth", auths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizeAuthFileAppliesSourceDisabledToPluginMultiAuths(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fullPath := filepath.Join(tempDir, "geminicli.json")
|
||||
raw := []byte(`{"type":"gemini-cli","disabled":true}`)
|
||||
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
|
||||
PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
|
||||
return []*coreauth.Auth{
|
||||
{ID: "geminicli.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli"}},
|
||||
{ID: "geminicli-project-a.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli", "project_id": "project-a"}},
|
||||
}, true, nil
|
||||
}),
|
||||
}
|
||||
|
||||
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 2 {
|
||||
t.Fatalf("SynthesizeAuthFile() len = %d, want two plugin auths", len(auths))
|
||||
}
|
||||
for _, auth := range auths {
|
||||
if !auth.Disabled || auth.Status != coreauth.StatusDisabled {
|
||||
t.Fatalf("auth %s disabled/status = %v/%s, want disabled", auth.ID, auth.Disabled, auth.Status)
|
||||
}
|
||||
if got, _ := auth.Metadata["disabled"].(bool); !got {
|
||||
t.Fatalf("auth %s metadata disabled = %#v, want true", auth.ID, auth.Metadata["disabled"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizeAuthFilePluginHandledEmptySuppressesBuiltin(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fullPath := filepath.Join(tempDir, "codex.json")
|
||||
raw := []byte(`{"type":"codex","access_token":"token"}`)
|
||||
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
|
||||
PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
|
||||
return nil, true, nil
|
||||
}),
|
||||
}
|
||||
|
||||
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 0 {
|
||||
t.Fatalf("SynthesizeAuthFile() len = %d, want plugin-handled empty result", len(auths))
|
||||
}
|
||||
}
|
||||
|
||||
type multiAuthParserFunc func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error)
|
||||
|
||||
func (f multiAuthParserFunc) ParseAuth(context.Context, pluginapi.AuthParseRequest) (*coreauth.Auth, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (f multiAuthParserFunc) ParseAuths(ctx context.Context, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
|
||||
return f(ctx, req)
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_SkipsInvalidFiles(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create various invalid files
|
||||
_ = os.WriteFile(filepath.Join(tempDir, "not-json.txt"), []byte("text content"), 0644)
|
||||
_ = os.WriteFile(filepath.Join(tempDir, "invalid.json"), []byte("not valid json"), 0644)
|
||||
_ = os.WriteFile(filepath.Join(tempDir, "empty.json"), []byte(""), 0644)
|
||||
_ = os.WriteFile(filepath.Join(tempDir, "no-type.json"), []byte(`{"email": "test@example.com"}`), 0644)
|
||||
|
||||
// Create one valid file
|
||||
validData, _ := json.Marshal(map[string]any{"type": "claude", "email": "valid@example.com"})
|
||||
_ = os.WriteFile(filepath.Join(tempDir, "valid.json"), validData, 0644)
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("only valid auth file should be processed, got %d", len(auths))
|
||||
}
|
||||
if auths[0].Label != "valid@example.com" {
|
||||
t.Errorf("expected label valid@example.com, got %s", auths[0].Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_SkipsDirectories(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create a subdirectory with a json file inside
|
||||
subDir := filepath.Join(tempDir, "subdir.json")
|
||||
err := os.Mkdir(subDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subdir: %v", err)
|
||||
}
|
||||
|
||||
// Create a valid file in root
|
||||
validData, _ := json.Marshal(map[string]any{"type": "claude"})
|
||||
_ = os.WriteFile(filepath.Join(tempDir, "valid.json"), validData, 0644)
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_RelativeID(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authData := map[string]any{"type": "claude"}
|
||||
data, _ := json.Marshal(authData)
|
||||
err := os.WriteFile(filepath.Join(tempDir, "my-auth.json"), data, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write auth file: %v", err)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
|
||||
// ID should be relative path
|
||||
if auths[0].ID != "my-auth.json" {
|
||||
t.Errorf("expected ID my-auth.json, got %s", auths[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_PrefixValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
wantPrefix string
|
||||
}{
|
||||
{"valid prefix", "myprefix", "myprefix"},
|
||||
{"prefix with slashes trimmed", "/myprefix/", "myprefix"},
|
||||
{"prefix with spaces trimmed", " myprefix ", "myprefix"},
|
||||
{"prefix with internal slash rejected", "my/prefix", ""},
|
||||
{"empty prefix", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
authData := map[string]any{
|
||||
"type": "claude",
|
||||
"prefix": tt.prefix,
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
_ = os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
if auths[0].Prefix != tt.wantPrefix {
|
||||
t.Errorf("expected prefix %q, got %q", tt.wantPrefix, auths[0].Prefix)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_PriorityParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
priority any
|
||||
want string
|
||||
hasValue bool
|
||||
}{
|
||||
{
|
||||
name: "string with spaces",
|
||||
priority: " 10 ",
|
||||
want: "10",
|
||||
hasValue: true,
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
priority: 8,
|
||||
want: "8",
|
||||
hasValue: true,
|
||||
},
|
||||
{
|
||||
name: "invalid string",
|
||||
priority: "1x",
|
||||
hasValue: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
authData := map[string]any{
|
||||
"type": "claude",
|
||||
"priority": tt.priority,
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
|
||||
if errWriteFile != nil {
|
||||
t.Fatalf("failed to write auth file: %v", errWriteFile)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, errSynthesize := synth.Synthesize(ctx)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("unexpected error: %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
|
||||
value, ok := auths[0].Attributes["priority"]
|
||||
if tt.hasValue {
|
||||
if !ok {
|
||||
t.Fatal("expected priority attribute to be set")
|
||||
}
|
||||
if value != tt.want {
|
||||
t.Fatalf("expected priority %q, got %q", tt.want, value)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("expected priority attribute to be absent, got %q", value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_WeightParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
weight any
|
||||
want string
|
||||
valid bool
|
||||
}{
|
||||
{name: "number", weight: 5, want: "5", valid: true},
|
||||
{name: "numeric string", weight: " 3 ", want: "3", valid: true},
|
||||
{name: "zero excludes", weight: 0, want: "0", valid: true},
|
||||
{name: "negative excludes", weight: -5, want: "0", valid: true},
|
||||
{name: "maximum", weight: 1000000, want: "1000000", valid: true},
|
||||
{name: "fraction rejected", weight: 1.5},
|
||||
{name: "above maximum rejected", weight: 1000001},
|
||||
{name: "overflow rejected", weight: "9223372036854775808"},
|
||||
{name: "invalid string", weight: "heavy"},
|
||||
}
|
||||
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data, errMarshal := json.Marshal(map[string]any{"type": "claude", "weight": testCase.weight})
|
||||
if errMarshal != nil {
|
||||
t.Fatalf("json.Marshal() error = %v", errMarshal)
|
||||
}
|
||||
if errWrite := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644); errWrite != nil {
|
||||
t.Fatalf("WriteFile() error = %v", errWrite)
|
||||
}
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
auths, errSynthesize := NewFileSynthesizer().Synthesize(ctx)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("Synthesize() error = %v", errSynthesize)
|
||||
}
|
||||
if !testCase.valid {
|
||||
if len(auths) != 0 {
|
||||
t.Fatalf("auth count = %d, want invalid credential skipped", len(auths))
|
||||
}
|
||||
if _, errDirect := SynthesizeAuthFile(ctx, filepath.Join(tempDir, "auth.json"), data); errDirect == nil {
|
||||
t.Fatal("SynthesizeAuthFile() error = nil, want weight validation error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("auth count = %d, want 1", len(auths))
|
||||
}
|
||||
if gotWeight := auths[0].Attributes[coreauth.AttributeWeight]; gotWeight != testCase.want {
|
||||
t.Fatalf("weight = %q, want %q", gotWeight, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_OAuthExcludedModelsMerged(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
authData := map[string]any{
|
||||
"type": "claude",
|
||||
"excluded_models": []string{"custom-model", "MODEL-B"},
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
|
||||
if errWriteFile != nil {
|
||||
t.Fatalf("failed to write auth file: %v", errWriteFile)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{
|
||||
OAuthExcludedModels: map[string][]string{
|
||||
"claude": {"shared", "model-b"},
|
||||
},
|
||||
},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, errSynthesize := synth.Synthesize(ctx)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("unexpected error: %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
|
||||
got := auths[0].Attributes["excluded_models"]
|
||||
want := "custom-model,model-b,shared"
|
||||
if got != want {
|
||||
t.Fatalf("expected excluded_models %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_OAuthModelAliases(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
authData := map[string]any{
|
||||
"type": "codex",
|
||||
"email": "codex@example.com",
|
||||
"model_aliases": []map[string]any{
|
||||
{"name": " gpt-5.3-codex-spark ", "alias": " gpt-5.5 "},
|
||||
{"name": "gpt-5.3-codex-spark", "alias": "gpt-5.4", "fork": true},
|
||||
{"name": "gpt-5.3-codex-spark", "alias": "gpt-5.5"},
|
||||
{"name": "", "alias": "ignored"},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
errWriteFile := os.WriteFile(filepath.Join(tempDir, "codex-auth.json"), data, 0644)
|
||||
if errWriteFile != nil {
|
||||
t.Fatalf("failed to write auth file: %v", errWriteFile)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, errSynthesize := synth.Synthesize(ctx)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("unexpected error: %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
|
||||
got := auths[0].Attributes["model_aliases"]
|
||||
want := `[{"name":"gpt-5.3-codex-spark","alias":"gpt-5.5"},{"name":"gpt-5.3-codex-spark","alias":"gpt-5.4","fork":true}]`
|
||||
if got != want {
|
||||
t.Fatalf("expected model_aliases %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_IgnoresGeminiOAuthFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authData := map[string]any{
|
||||
"type": "gemini",
|
||||
"email": "multi@example.com",
|
||||
"project_id": "project-a, project-b, project-c",
|
||||
"priority": " 10 ",
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
err := os.WriteFile(filepath.Join(tempDir, "gemini-multi.json"), data, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write auth file: %v", err)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(auths) != 0 {
|
||||
t.Fatalf("expected Gemini auth file to be ignored, got %d auths", len(auths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_NoteParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
note any
|
||||
want string
|
||||
hasValue bool
|
||||
}{
|
||||
{
|
||||
name: "valid string note",
|
||||
note: "hello world",
|
||||
want: "hello world",
|
||||
hasValue: true,
|
||||
},
|
||||
{
|
||||
name: "string note with whitespace",
|
||||
note: " trimmed note ",
|
||||
want: "trimmed note",
|
||||
hasValue: true,
|
||||
},
|
||||
{
|
||||
name: "empty string note",
|
||||
note: "",
|
||||
hasValue: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace only note",
|
||||
note: " ",
|
||||
hasValue: false,
|
||||
},
|
||||
{
|
||||
name: "non-string note ignored",
|
||||
note: 12345,
|
||||
hasValue: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
authData := map[string]any{
|
||||
"type": "claude",
|
||||
"note": tt.note,
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
|
||||
if errWriteFile != nil {
|
||||
t.Fatalf("failed to write auth file: %v", errWriteFile)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, errSynthesize := synth.Synthesize(ctx)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("unexpected error: %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
|
||||
value, ok := auths[0].Attributes["note"]
|
||||
if tt.hasValue {
|
||||
if !ok {
|
||||
t.Fatal("expected note attribute to be set")
|
||||
}
|
||||
if value != tt.want {
|
||||
t.Fatalf("expected note %q, got %q", tt.want, value)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("expected note attribute to be absent, got %q", value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue