Files
zero/internal/cli/mcp_default_test.go
1ec7219aee feat: use Exa as default search provider (#926)
* feat: use Exa as default search provider

Amp-Thread-ID: https://ampcode.com/threads/T-01a01522-a5f1-7139-8a55-5e417cba2751
Co-authored-by: Amp <[email protected]>

* fix(config): carry a legacy default's disable across the Exa migration

A user who ran `zero mcp disable firecrawl` chose not to open that outbound
connection. Renaming the built-in default to exa re-opened it under the new
name on upgrade, and because the replacement looked like an untouched default
the startup warning stayed quiet too.

Carry an explicit user-level disable of a retired default onto its successor,
but only when the user never declared the successor themselves — an explicit
exa entry wins either way. The carried-over disable is recorded as a user-level
decision, so the lower-trust project layer cannot lift it while
`zero mcp enable exa` still can.

Regression coverage: the legacy-disabled upgrade path, the explicit-exa
override, a configured-but-enabled legacy entry, the CLI re-enable, the
project-layer re-enable attempt, and the resulting startup behavior
(NormalizeConfig must not hand back an exa server to connect to).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* fix(config): keep a retired default's transport for partial user entries

A user who added an API key to the firecrawl default wrote only the header —
the seeded default supplied the http type and URL. Once the default stops being
seeded that entry resolves with no type, url, or command, and NormalizeConfig
rejects it, taking down startup for every other server too rather than just
that one.

Record what Zero used to ship for a retired default and re-seed it under the
user's own fields. Only for an entry that named no transport of its own: an
entry with a self-hosted url or a stdio command is a complete definition the
user owns, and grafting the retired http default onto a command produces a
server that is rejected outright.

Regression coverage at both ResolveMCP and NormalizeConfig for the header-only
entry and its failure path, the self-hosted stdio entry that must be left
alone, and a disabled retired entry keeping a usable transport so a later
re-enable resolves.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

---------

Co-authored-by: Amp <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-08-20 20:26:24 +08:00

47 lines
1.5 KiB
Go

package cli
import (
"bytes"
"encoding/json"
"path/filepath"
"testing"
"github.com/Gitlawb/zero/internal/config"
)
// `zero mcp disable exa` must work even though exa is a built-in
// default that is not written to the user's config file until overridden.
func TestRunMCPDisableSeededExaDefault(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "zero", "config.json")
// A config with no Exa entry — the default lives in code, not the file.
writeMCPCommandRawConfig(t, configPath, `{"activeProvider":"fast"}`)
var out, errBuf bytes.Buffer
code := runWithDeps([]string{"mcp", "disable", "exa", "--json"}, &out, &errBuf, appDeps{
userConfigPath: func() (string, error) { return configPath, nil },
})
if code != exitSuccess {
t.Fatalf("disable exit=%d stderr=%s", code, errBuf.String())
}
var payload struct {
ServerName string `json:"serverName"`
Disabled bool `json:"disabled"`
Changed bool `json:"changed"`
}
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
t.Fatalf("decode disable JSON: %v\n%s", err, out.String())
}
if payload.ServerName != "exa" || !payload.Disabled || !payload.Changed {
t.Fatalf("disable payload = %#v, want exa disabled+changed", payload)
}
// End-to-end: resolving that config now turns the default off.
cfg, err := config.ResolveMCP(config.ResolveOptions{UserConfigPath: configPath})
if err != nil {
t.Fatalf("ResolveMCP: %v", err)
}
if !cfg.Servers["exa"].Disabled {
t.Fatal("expected `mcp disable exa` to turn the seeded default off in the resolved config")
}
}