diff --git a/internal/account/pool_test.go b/internal/account/pool_test.go index f19f243..37109ff 100644 --- a/internal/account/pool_test.go +++ b/internal/account/pool_test.go @@ -194,7 +194,7 @@ func TestPoolAccountConcurrencyAliasEnv(t *testing.T) { } } -func TestPoolSkipsTokenOnlyAccount(t *testing.T) { +func TestPoolDropsLegacyTokenOnlyAccountOnLoad(t *testing.T) { t.Setenv("DS2API_ACCOUNT_MAX_INFLIGHT", "1") t.Setenv("DS2API_CONFIG_JSON", `{ "keys":["k1"], @@ -203,7 +203,7 @@ func TestPoolSkipsTokenOnlyAccount(t *testing.T) { pool := NewPool(config.LoadStore()) status := pool.Status() - if got, ok := status["total"].(int); !ok || got != 1 { + if got, ok := status["total"].(int); !ok || got != 0 { t.Fatalf("unexpected total in pool status: %#v", status["total"]) } if got, ok := status["available"].(int); !ok || got != 0 { diff --git a/internal/config/config.go b/internal/config/config.go index 7264e9d..8c50f8e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,6 +35,23 @@ func (c *Config) ClearAccountTokens() { } } +// DropInvalidAccounts removes accounts that cannot be addressed by admin APIs +// (no email and no normalizable mobile). This prevents legacy token-only +// records from becoming orphaned empty entries after token stripping. +func (c *Config) DropInvalidAccounts() { + if c == nil || len(c.Accounts) == 0 { + return + } + kept := make([]Account, 0, len(c.Accounts)) + for _, acc := range c.Accounts { + if acc.Identifier() == "" { + continue + } + kept = append(kept, acc) + } + c.Accounts = kept +} + type CompatConfig struct { WideInputStrictOutput *bool `json:"wide_input_strict_output,omitempty"` } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5a0f9b7..b87b118 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -29,6 +29,27 @@ func TestLoadStoreClearsTokensFromConfigInput(t *testing.T) { } } +func TestLoadStoreDropsLegacyTokenOnlyAccounts(t *testing.T) { + t.Setenv("DS2API_CONFIG_JSON", `{ + "accounts":[ + {"token":"legacy-token-only"}, + {"email":"u@example.com","password":"p","token":"runtime-token"} + ] + }`) + + store := LoadStore() + accounts := store.Accounts() + if len(accounts) != 1 { + t.Fatalf("expected token-only account to be dropped, got %d accounts", len(accounts)) + } + if accounts[0].Identifier() != "u@example.com" { + t.Fatalf("unexpected remaining account: %#v", accounts[0]) + } + if accounts[0].Token != "" { + t.Fatalf("expected persisted token to be cleared, got %q", accounts[0].Token) + } +} + func TestStoreUpdateAccountTokenKeepsIdentifierResolvable(t *testing.T) { t.Setenv("DS2API_CONFIG_JSON", `{ "accounts":[{"email":"user@example.com","password":"p"}] diff --git a/internal/config/store.go b/internal/config/store.go index 8b734d0..10e26f4 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -40,6 +40,7 @@ func loadConfig() (Config, bool, error) { if rawCfg != "" { cfg, err := parseConfigString(rawCfg) cfg.ClearAccountTokens() + cfg.DropInvalidAccounts() return cfg, true, err } @@ -57,6 +58,7 @@ func loadConfig() (Config, bool, error) { return Config{}, false, err } cfg.ClearAccountTokens() + cfg.DropInvalidAccounts() if IsVercel() { // Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors. return cfg, true, nil