mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 17:56:14 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5068a3840f | ||
|
|
2a5b2af277 |
No files matched your search
@@ -142,13 +142,6 @@ the key in the wizard:
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export ANTHROPIC_API_KEY=...
|
||||
export GEMINI_API_KEY=...
|
||||
export LONGCAT_API_KEY=...
|
||||
```
|
||||
|
||||
To configure Meituan LongCat (LongCat-2.0) directly, run:
|
||||
|
||||
```bash
|
||||
zero providers setup longcat --set-active
|
||||
```
|
||||
|
||||
For local models, run Ollama or LM Studio and then use `zero setup` or
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -65,7 +66,7 @@ func runDoctor(args []string, stdout io.Writer, stderr io.Writer, deps appDeps)
|
||||
|
||||
report := doctor.Run(doctor.Options{
|
||||
Now: deps.now,
|
||||
Runtime: "go",
|
||||
Runtime: runtime.Version(),
|
||||
UserConfig: userConfig,
|
||||
ProjectConfig: projectConfig,
|
||||
Provider: provider,
|
||||
|
||||
@@ -18,7 +18,6 @@ func TestProviderCatalogRuntimeProvidersShowOnboardingSetupHints(t *testing.T) {
|
||||
}{
|
||||
{id: "openai", name: "OpenAI", transport: "openai", defaultModel: "gpt-4.1"},
|
||||
{id: "groq", name: "Groq", transport: "openai-compatible", defaultModel: "llama-3.3-70b-versatile"},
|
||||
{id: "longcat", name: "LongCat", transport: "openai-compatible", defaultModel: "LongCat-2.0"},
|
||||
{id: "ollama-cloud", name: "Ollama Cloud", transport: "openai-compatible", defaultModel: "qwen3-coder:480b"},
|
||||
{id: "ollama", name: "Ollama Local", transport: "openai-compatible", defaultModel: "llama3.1"},
|
||||
}
|
||||
@@ -47,21 +46,6 @@ func TestProviderCatalogRuntimeProvidersShowOnboardingSetupHints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCatalogLongCatRequiresAPIKey(t *testing.T) {
|
||||
output := runProviderCatalogOnboarding(t)
|
||||
block := providerCatalogOnboardingBlock(t, output, "longcat")
|
||||
|
||||
for _, want := range []string{
|
||||
"requiresAuth=true",
|
||||
"authEnvVars=LONGCAT_API_KEY",
|
||||
"setup: zero providers setup longcat --set-active",
|
||||
} {
|
||||
if !strings.Contains(block, want) {
|
||||
t.Fatalf("expected LongCat catalog block to contain %q, got:\n%s", want, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCatalogOllamaCloudRequiresAPIKey(t *testing.T) {
|
||||
output := runProviderCatalogOnboarding(t)
|
||||
block := providerCatalogOnboardingBlock(t, output, "ollama-cloud")
|
||||
|
||||
@@ -57,6 +57,11 @@ type Options struct {
|
||||
// LSP-server checks. Nil means exec.LookPath; tests inject a stub so the
|
||||
// checks are deterministic regardless of the host's installed tooling.
|
||||
LookupExecutable func(string) (string, error)
|
||||
// Executable resolves the running Zero binary's path (nil => os.Executable)
|
||||
// and StatExecutable stats it (nil => os.Stat). Injected so the native-binary
|
||||
// runtime check is testable without depending on a real on-disk layout.
|
||||
Executable func() (string, error)
|
||||
StatExecutable func(string) (os.FileInfo, error)
|
||||
}
|
||||
|
||||
func Run(options Options) Report {
|
||||
@@ -65,7 +70,7 @@ func Run(options Options) Report {
|
||||
now = time.Now
|
||||
}
|
||||
checks := []Check{
|
||||
runtimeCheck(options.Runtime),
|
||||
runtimeCheck(options),
|
||||
configFilesCheck(options.UserConfig, options.ProjectConfig),
|
||||
configValidationCheck(options.UserConfig, options.ProjectConfig),
|
||||
}
|
||||
@@ -114,12 +119,48 @@ func Format(report Report) string {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func runtimeCheck(runtime string) Check {
|
||||
runtime = strings.TrimSpace(runtime)
|
||||
if runtime == "" {
|
||||
runtime = "go"
|
||||
// runtimeCheck verifies the running Zero native binary is actually present and
|
||||
// runnable, and reports its resolved path — instead of the former hardcoded
|
||||
// always-pass, which claimed "Go runtime is available" while checking nothing
|
||||
// (#405). doctor executes INSIDE the native binary, so this only fails if
|
||||
// os.Executable can't resolve or the running file has vanished; the missing-
|
||||
// binary-at-install case is caught earlier by the npm wrapper, which refuses to
|
||||
// launch without the platform binary. Surfacing the binary PATH is the useful
|
||||
// part: it lets a user see which binary answered (e.g. an npm-installed one vs a
|
||||
// stale/shadowing copy on PATH) rather than a meaningless green.
|
||||
func runtimeCheck(options Options) Check {
|
||||
runtimeVersion := strings.TrimSpace(options.Runtime)
|
||||
if runtimeVersion == "" {
|
||||
runtimeVersion = "go"
|
||||
}
|
||||
return check("runtime.go", "Go runtime", StatusPass, fmt.Sprintf("Zero Go runtime is available (%s).", runtime), map[string]any{"runtime": runtime})
|
||||
details := map[string]any{"runtime": runtimeVersion}
|
||||
|
||||
executable := options.Executable
|
||||
if executable == nil {
|
||||
executable = os.Executable
|
||||
}
|
||||
path, err := executable()
|
||||
if err != nil {
|
||||
return check("runtime.go", "Zero native binary", StatusFail,
|
||||
"Could not resolve the running Zero binary: "+err.Error(), details)
|
||||
}
|
||||
details["binaryPath"] = path
|
||||
|
||||
stat := options.StatExecutable
|
||||
if stat == nil {
|
||||
stat = os.Stat
|
||||
}
|
||||
info, err := stat(path)
|
||||
if err != nil {
|
||||
return check("runtime.go", "Zero native binary", StatusFail,
|
||||
fmt.Sprintf("The running Zero binary is not accessible at %s: %s. Reinstall the zero package, or build from source: https://github.com/Gitlawb/zero", path, err.Error()), details)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return check("runtime.go", "Zero native binary", StatusFail,
|
||||
fmt.Sprintf("The resolved Zero binary path %s is a directory, not an executable.", path), details)
|
||||
}
|
||||
return check("runtime.go", "Zero native binary", StatusPass,
|
||||
fmt.Sprintf("Zero native binary is present and runnable at %s (%s).", path, runtimeVersion), details)
|
||||
}
|
||||
|
||||
func configFilesCheck(userPath string, projectPath string) Check {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakeFileInfo is a minimal os.FileInfo for the runtime-check stat injection.
|
||||
type fakeFileInfo struct{ dir bool }
|
||||
|
||||
func (f fakeFileInfo) Name() string { return "zero" }
|
||||
func (f fakeFileInfo) Size() int64 { return 1 }
|
||||
func (f fakeFileInfo) Mode() fs.FileMode { return 0o755 }
|
||||
func (f fakeFileInfo) ModTime() time.Time { return time.Unix(0, 0) }
|
||||
func (f fakeFileInfo) IsDir() bool { return f.dir }
|
||||
func (f fakeFileInfo) Sys() any { return nil }
|
||||
|
||||
func runtimeCheckFor(t *testing.T, exe func() (string, error), stat func(string) (os.FileInfo, error)) Check {
|
||||
t.Helper()
|
||||
return runtimeCheck(Options{Runtime: "go1.26", Executable: exe, StatExecutable: stat})
|
||||
}
|
||||
|
||||
// A present, runnable binary passes AND reports its resolved path — the check is
|
||||
// no longer a hardcoded green; it names which binary answered.
|
||||
func TestRuntimeCheckPassesAndReportsBinaryPath(t *testing.T) {
|
||||
c := runtimeCheckFor(t,
|
||||
func() (string, error) { return "/opt/homebrew/lib/node_modules/@gitlawb/zero/zero", nil },
|
||||
func(string) (os.FileInfo, error) { return fakeFileInfo{}, nil })
|
||||
if c.Status != StatusPass {
|
||||
t.Fatalf("status = %s, want pass", c.Status)
|
||||
}
|
||||
if c.Details["binaryPath"] != "/opt/homebrew/lib/node_modules/@gitlawb/zero/zero" {
|
||||
t.Fatalf("binaryPath = %v, want the resolved path in details", c.Details["binaryPath"])
|
||||
}
|
||||
if !strings.Contains(c.Message, "/opt/homebrew/lib/node_modules/@gitlawb/zero/zero") {
|
||||
t.Fatalf("message should surface the binary path, got %q", c.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// The vanished-binary case FAILS loudly instead of the old unconditional pass —
|
||||
// this is the #405 regression: a broken runtime no longer gets a green check.
|
||||
func TestRuntimeCheckFailsWhenBinaryMissing(t *testing.T) {
|
||||
c := runtimeCheckFor(t,
|
||||
func() (string, error) { return "/path/to/zero", nil },
|
||||
func(string) (os.FileInfo, error) { return nil, os.ErrNotExist })
|
||||
if c.Status != StatusFail {
|
||||
t.Fatalf("#405: a missing/inaccessible binary must FAIL, got %s (%q)", c.Status, c.Message)
|
||||
}
|
||||
if !strings.Contains(c.Message, "/path/to/zero") {
|
||||
t.Fatalf("failure message should name the path, got %q", c.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// os.Executable resolution failure fails loudly too.
|
||||
func TestRuntimeCheckFailsWhenExecutableUnresolvable(t *testing.T) {
|
||||
c := runtimeCheckFor(t,
|
||||
func() (string, error) { return "", errors.New("no executable path") },
|
||||
func(string) (os.FileInfo, error) { return fakeFileInfo{}, nil })
|
||||
if c.Status != StatusFail {
|
||||
t.Fatalf("unresolvable executable must FAIL, got %s", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// A path that resolves to a directory is not a runnable binary → FAIL.
|
||||
func TestRuntimeCheckFailsWhenPathIsDirectory(t *testing.T) {
|
||||
c := runtimeCheckFor(t,
|
||||
func() (string, error) { return "/some/dir", nil },
|
||||
func(string) (os.FileInfo, error) { return fakeFileInfo{dir: true}, nil })
|
||||
if c.Status != StatusFail {
|
||||
t.Fatalf("a directory path must FAIL, got %s", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// Default injection (nil Executable/StatExecutable) resolves the real running
|
||||
// test binary — proving the production default path works and passes.
|
||||
func TestRuntimeCheckDefaultsToRealBinary(t *testing.T) {
|
||||
c := runtimeCheck(Options{})
|
||||
if c.Status != StatusPass {
|
||||
t.Fatalf("default runtime check should pass for the running test binary, got %s (%q)", c.Status, c.Message)
|
||||
}
|
||||
if strings.TrimSpace(c.Details["binaryPath"].(string)) == "" {
|
||||
t.Fatal("default runtime check should report the running binary path")
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,6 @@ var descriptors = []Descriptor{
|
||||
openAICompat("together", "Together AI", "https://api.together.xyz/v1", "meta-llama/Llama-3.3-70B-Instruct-Turbo", []string{"TOGETHER_API_KEY"}),
|
||||
openAICompat("dashscope", "DashScope", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "qwen-plus", []string{"DASHSCOPE_API_KEY", "QWEN_API_KEY"}, "qwen"),
|
||||
openAICompat("moonshot", "Moonshot AI", "https://api.moonshot.ai/v1", "kimi-k2-0905-preview", []string{"MOONSHOT_API_KEY"}, "kimi"),
|
||||
openAICompat("longcat", "LongCat", "https://api.longcat.chat/openai", "LongCat-2.0", []string{"LONGCAT_API_KEY"}, "meituan longcat", "meituan", "longcat-2.0"),
|
||||
openAICompat("nvidia-nim", "NVIDIA NIM", "https://integrate.api.nvidia.com/v1", "nvidia/llama-3.1-nemotron-70b-instruct", []string{"NVIDIA_API_KEY"}, "nvidia nim"),
|
||||
anthropicCompat("minimax", "MiniMax", "https://api.minimax.io/anthropic", "MiniMax-M3", []string{"MINIMAX_API_KEY"}, "mini-max", "mini_max"),
|
||||
openAICompat("mistral", "Mistral", "https://api.mistral.ai/v1", "mistral-large-latest", []string{"MISTRAL_API_KEY"}),
|
||||
|
||||
@@ -23,7 +23,6 @@ var expectedCatalogIDs = []string{
|
||||
"together",
|
||||
"dashscope",
|
||||
"moonshot",
|
||||
"longcat",
|
||||
"nvidia-nim",
|
||||
"minimax",
|
||||
"mistral",
|
||||
@@ -100,28 +99,6 @@ func TestRecommendedProviderEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongCatDescriptor(t *testing.T) {
|
||||
descriptor, err := Require("longcat")
|
||||
if err != nil {
|
||||
t.Fatalf("Require(longcat) error = %v", err)
|
||||
}
|
||||
if descriptor.Name != "LongCat" {
|
||||
t.Fatalf("Name = %q, want LongCat", descriptor.Name)
|
||||
}
|
||||
if descriptor.DefaultBaseURL != "https://api.longcat.chat/openai" {
|
||||
t.Fatalf("DefaultBaseURL = %q, want LongCat OpenAI-compatible endpoint", descriptor.DefaultBaseURL)
|
||||
}
|
||||
if descriptor.DefaultModel != "LongCat-2.0" {
|
||||
t.Fatalf("DefaultModel = %q, want LongCat-2.0", descriptor.DefaultModel)
|
||||
}
|
||||
if descriptor.Transport != TransportOpenAICompatible {
|
||||
t.Fatalf("Transport = %q, want %q", descriptor.Transport, TransportOpenAICompatible)
|
||||
}
|
||||
if !reflect.DeepEqual(descriptor.AuthEnvVars, []string{"LONGCAT_API_KEY"}) {
|
||||
t.Fatalf("AuthEnvVars = %#v, want LONGCAT_API_KEY", descriptor.AuthEnvVars)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogDescriptorsExposeRequiredDefaults(t *testing.T) {
|
||||
for _, descriptor := range All() {
|
||||
if descriptor.ID == "" {
|
||||
@@ -290,7 +267,7 @@ func TestListByTransportPreservesCatalogOrder(t *testing.T) {
|
||||
TransportBedrock: {"bedrock"},
|
||||
TransportVertex: {"vertex"},
|
||||
TransportAnthropicCompat: {"minimax", "custom-anthropic-compatible"},
|
||||
TransportOpenAICompat: {"gitlawb-opengateway", "ollama-cloud", "ollama", "lmstudio", "openrouter", "huggingface", "chatgpt", "groq", "deepseek", "together", "dashscope", "moonshot", "longcat", "nvidia-nim", "mistral", "github", "xai", "venice", "xiaomi-mimo", "bankr", "zai", "kilocode", "opencode", "opencode-go", "atomic-chat", "chatgpt-proxy", "custom-openai-compatible"},
|
||||
TransportOpenAICompat: {"gitlawb-opengateway", "ollama-cloud", "ollama", "lmstudio", "openrouter", "huggingface", "chatgpt", "groq", "deepseek", "together", "dashscope", "moonshot", "nvidia-nim", "mistral", "github", "xai", "venice", "xiaomi-mimo", "bankr", "zai", "kilocode", "opencode", "opencode-go", "atomic-chat", "chatgpt-proxy", "custom-openai-compatible"},
|
||||
}
|
||||
|
||||
for transport, wantIDs := range cases {
|
||||
|
||||
Reference in new issue
Block a user