-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(api): expose priority and note in GET /auth-files response #2124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
5b6342e
f90120f
8d8f597
c1241a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -332,6 +332,21 @@ func (h *Handler) listAuthFilesFromDisk(c *gin.Context) { | |
| emailValue := gjson.GetBytes(data, "email").String() | ||
| fileData["type"] = typeValue | ||
| fileData["email"] = emailValue | ||
| if pv := gjson.GetBytes(data, "priority"); pv.Exists() { | ||
| switch pv.Type { | ||
| case gjson.Number: | ||
| fileData["priority"] = int(pv.Int()) | ||
| case gjson.String: | ||
| if parsed, errAtoi := strconv.Atoi(strings.TrimSpace(pv.String())); errAtoi == nil { | ||
| fileData["priority"] = parsed | ||
| } | ||
| } | ||
| } | ||
| if nv := gjson.GetBytes(data, "note"); nv.Exists() { | ||
| if trimmed := strings.TrimSpace(nv.String()); trimmed != "" { | ||
| fileData["note"] = trimmed | ||
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| files = append(files, fileData) | ||
|
|
@@ -415,6 +430,16 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H { | |
| if claims := extractCodexIDTokenClaims(auth); claims != nil { | ||
| entry["id_token"] = claims | ||
| } | ||
| // Expose priority from Attributes (set by synthesizer from JSON "priority" field). | ||
| if p := strings.TrimSpace(authAttribute(auth, "priority")); p != "" { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| if parsed, err := strconv.Atoi(p); err == nil { | ||
| entry["priority"] = parsed | ||
| } | ||
| } | ||
| // Expose note from Attributes (set by synthesizer from JSON "note" field). | ||
| if note := strings.TrimSpace(authAttribute(auth, "note")); note != "" { | ||
| entry["note"] = note | ||
| } | ||
| return entry | ||
| } | ||
|
|
||
|
|
@@ -839,7 +864,7 @@ func (h *Handler) PatchAuthFileStatus(c *gin.Context) { | |
| c.JSON(http.StatusOK, gin.H{"status": "ok", "disabled": *req.Disabled}) | ||
| } | ||
|
|
||
| // PatchAuthFileFields updates editable fields (prefix, proxy_url, priority) of an auth file. | ||
| // PatchAuthFileFields updates editable fields (prefix, proxy_url, priority, note) of an auth file. | ||
| func (h *Handler) PatchAuthFileFields(c *gin.Context) { | ||
| if h.authManager == nil { | ||
| c.JSON(http.StatusServiceUnavailable, gin.H{"error": "core auth manager unavailable"}) | ||
|
|
@@ -851,6 +876,7 @@ func (h *Handler) PatchAuthFileFields(c *gin.Context) { | |
| Prefix *string `json:"prefix"` | ||
| ProxyURL *string `json:"proxy_url"` | ||
| Priority *int `json:"priority"` | ||
| Note *string `json:"note"` | ||
| } | ||
| if err := c.ShouldBindJSON(&req); err != nil { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) | ||
|
|
@@ -893,14 +919,32 @@ func (h *Handler) PatchAuthFileFields(c *gin.Context) { | |
| targetAuth.ProxyURL = *req.ProxyURL | ||
| changed = true | ||
| } | ||
| if req.Priority != nil { | ||
| if req.Priority != nil || req.Note != nil { | ||
| if targetAuth.Metadata == nil { | ||
| targetAuth.Metadata = make(map[string]any) | ||
| } | ||
| if *req.Priority == 0 { | ||
| delete(targetAuth.Metadata, "priority") | ||
| } else { | ||
| targetAuth.Metadata["priority"] = *req.Priority | ||
| if targetAuth.Attributes == nil { | ||
| targetAuth.Attributes = make(map[string]string) | ||
| } | ||
|
|
||
| if req.Priority != nil { | ||
| if *req.Priority == 0 { | ||
| delete(targetAuth.Metadata, "priority") | ||
| delete(targetAuth.Attributes, "priority") | ||
| } else { | ||
| targetAuth.Metadata["priority"] = *req.Priority | ||
| targetAuth.Attributes["priority"] = strconv.Itoa(*req.Priority) | ||
| } | ||
| } | ||
| if req.Note != nil { | ||
| trimmedNote := strings.TrimSpace(*req.Note) | ||
| if trimmedNote == "" { | ||
| delete(targetAuth.Metadata, "note") | ||
| delete(targetAuth.Attributes, "note") | ||
| } else { | ||
| targetAuth.Metadata["note"] = trimmedNote | ||
| targetAuth.Attributes["note"] = trimmedNote | ||
| } | ||
| } | ||
| changed = true | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -744,3 +744,200 @@ func TestBuildGeminiVirtualID(t *testing.T) { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestSynthesizeGeminiVirtualAuths_NotePropagated(t *testing.T) { | ||
| now := time.Now() | ||
| primary := &coreauth.Auth{ | ||
| ID: "primary-id", | ||
| Provider: "gemini-cli", | ||
| Label: "[email protected]", | ||
| Attributes: map[string]string{ | ||
| "source": "test-source", | ||
| "path": "/path/to/auth", | ||
| "priority": "5", | ||
| "note": "my test note", | ||
| }, | ||
| } | ||
| metadata := map[string]any{ | ||
| "project_id": "proj-a, proj-b", | ||
| "email": "[email protected]", | ||
| "type": "gemini", | ||
| } | ||
|
|
||
| virtuals := SynthesizeGeminiVirtualAuths(primary, metadata, now) | ||
|
|
||
| if len(virtuals) != 2 { | ||
| t.Fatalf("expected 2 virtuals, got %d", len(virtuals)) | ||
| } | ||
|
|
||
| for i, v := range virtuals { | ||
| if got := v.Attributes["note"]; got != "my test note" { | ||
| t.Errorf("virtual %d: expected note %q, got %q", i, "my test note", got) | ||
| } | ||
| if got := v.Attributes["priority"]; got != "5" { | ||
| t.Errorf("virtual %d: expected priority %q, got %q", i, "5", got) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestSynthesizeGeminiVirtualAuths_NoteAbsentWhenEmpty(t *testing.T) { | ||
| now := time.Now() | ||
| primary := &coreauth.Auth{ | ||
| ID: "primary-id", | ||
| Provider: "gemini-cli", | ||
| Label: "[email protected]", | ||
| Attributes: map[string]string{ | ||
| "source": "test-source", | ||
| "path": "/path/to/auth", | ||
| }, | ||
| } | ||
| metadata := map[string]any{ | ||
| "project_id": "proj-a, proj-b", | ||
| "email": "[email protected]", | ||
| "type": "gemini", | ||
| } | ||
|
|
||
| virtuals := SynthesizeGeminiVirtualAuths(primary, metadata, now) | ||
|
|
||
| if len(virtuals) != 2 { | ||
| t.Fatalf("expected 2 virtuals, got %d", len(virtuals)) | ||
| } | ||
|
|
||
| for i, v := range virtuals { | ||
| if _, hasNote := v.Attributes["note"]; hasNote { | ||
| t.Errorf("virtual %d: expected no note attribute when primary has no note", i) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestFileSynthesizer_Synthesize_MultiProjectGeminiWithNote(t *testing.T) { | ||
| tempDir := t.TempDir() | ||
|
|
||
| authData := map[string]any{ | ||
| "type": "gemini", | ||
| "email": "[email protected]", | ||
| "project_id": "project-a, project-b", | ||
| "priority": 5, | ||
| "note": "production keys", | ||
| } | ||
| 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) | ||
| } | ||
| // Should have 3 auths: 1 primary (disabled) + 2 virtuals | ||
| if len(auths) != 3 { | ||
| t.Fatalf("expected 3 auths (1 primary + 2 virtuals), got %d", len(auths)) | ||
| } | ||
|
|
||
| primary := auths[0] | ||
| if gotNote := primary.Attributes["note"]; gotNote != "production keys" { | ||
| t.Errorf("expected primary note %q, got %q", "production keys", gotNote) | ||
| } | ||
|
|
||
| // Verify virtuals inherit note | ||
| for i := 1; i < len(auths); i++ { | ||
| v := auths[i] | ||
| if gotNote := v.Attributes["note"]; gotNote != "production keys" { | ||
| t.Errorf("expected virtual %d note %q, got %q", i, "production keys", gotNote) | ||
| } | ||
| if gotPriority := v.Attributes["priority"]; gotPriority != "5" { | ||
| t.Errorf("expected virtual %d priority %q, got %q", i, "5", gotPriority) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the auth manager is unavailable, the fallback path now emits
priorityfor any file that merely has aprioritykey, butgjson.Int()coerces non-numeric values (for example"priority":"high"or"priority":null) to0. This makes the API report a concrete priority that was never configured and can change sort/order behavior in degraded mode, while the normal in-memory path only includesprioritywhen integer parsing succeeds.Useful? React with 👍 / 👎.