feat(cli): agent-readable account list (reduced JSON view)
account list now routes to the agent role; an agent (EMCLI_KEY only) gets a JSON envelope of name/from/can_send, while the admin keeps the full text table. account add/edit/remove stay admin-only. Also emit the agent path's missing-key/open failure as a JSON Failure envelope (per spec), and update the stale run_test case that asserted the old admin-only behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// With only EMCLI_KEY set, `account list` emits the reduced JSON envelope:
|
||||
// name/from/can_send, and never the IMAP host or login username.
|
||||
func TestAccountListAgentJSONView(t *testing.T) {
|
||||
adminEnv(t) // both keys + initialized temp DB
|
||||
run(t, "account", "add", "--name", "work", "--mode", "RW",
|
||||
"--imap-host", "imap.example.com", "--smtp-host", "smtp.example.com",
|
||||
"--username", "login@example.com", "--from", "me@example.com")
|
||||
run(t, "account", "add", "--name", "alerts", "--mode", "RO",
|
||||
"--imap-host", "imap.example.com", "--username", "alerts@example.com")
|
||||
|
||||
// Drop the admin key → caller is an agent.
|
||||
t.Setenv("EMCLI_ADMIN_KEY", "")
|
||||
code, out, errOut := run(t, "account", "list")
|
||||
if code != 0 {
|
||||
t.Fatalf("agent account list should succeed: code=%d err=%q", code, errOut)
|
||||
}
|
||||
|
||||
var env struct {
|
||||
Error bool `json:"error"`
|
||||
Data struct {
|
||||
Accounts []struct {
|
||||
Name string `json:"name"`
|
||||
From string `json:"from"`
|
||||
CanSend bool `json:"can_send"`
|
||||
} `json:"accounts"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &env); err != nil {
|
||||
t.Fatalf("output is not the agent envelope: %v\n%s", err, out)
|
||||
}
|
||||
if env.Error || len(env.Data.Accounts) != 2 {
|
||||
t.Fatalf("want 2 accounts and no error, got %+v", env)
|
||||
}
|
||||
// The reduced view must not leak the IMAP host or the login username.
|
||||
if strings.Contains(out, "imap.example.com") || strings.Contains(out, "login@example.com") {
|
||||
t.Fatalf("agent view leaked host/username:\n%s", out)
|
||||
}
|
||||
|
||||
got := map[string]struct {
|
||||
from string
|
||||
canSend bool
|
||||
}{}
|
||||
for _, a := range env.Data.Accounts {
|
||||
got[a.Name] = struct {
|
||||
from string
|
||||
canSend bool
|
||||
}{a.From, a.CanSend}
|
||||
}
|
||||
if g := got["work"]; g.from != "me@example.com" || !g.canSend {
|
||||
t.Errorf("work: want from=me@example.com can_send=true, got %+v", g)
|
||||
}
|
||||
// alerts has no --from → SendFrom() falls back to the username.
|
||||
if g := got["alerts"]; g.from != "alerts@example.com" || g.canSend {
|
||||
t.Errorf("alerts: want from=alerts@example.com can_send=false, got %+v", g)
|
||||
}
|
||||
}
|
||||
|
||||
// With the admin key present, `account list` stays the full human-readable table.
|
||||
func TestAccountListAdminTextView(t *testing.T) {
|
||||
adminEnv(t)
|
||||
run(t, "account", "add", "--name", "work", "--mode", "RW",
|
||||
"--imap-host", "imap.example.com", "--smtp-host", "smtp.example.com",
|
||||
"--username", "login@example.com", "--from", "me@example.com")
|
||||
|
||||
code, out, _ := run(t, "account", "list")
|
||||
if code != 0 {
|
||||
t.Fatalf("admin account list failed: code=%d", code)
|
||||
}
|
||||
for _, want := range []string{"NAME", "MODE", "IMAP", "USER", "imap.example.com:993", "login@example.com"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("admin view missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, `"accounts"`) {
|
||||
t.Fatalf("admin view should be text, not JSON:\n%s", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user