Files
zero/internal/cli/provider_detect_test.go
KRATOS 29fc29a41d feat: add providers detect and sandbox check (wire audit-found unwired features) + coverage (#238)
* feat(providers): add `zero providers detect` for local-runtime onboarding

Wire the previously-unwired provideronboarding detect/advice surface into the
CLI. `zero providers detect` probes for running local OpenAI-compatible runtimes
(Ollama, LM Studio) on their default ports and prints a no-key adopt command for
each, followed by the next-step actions (use / check / set key) for every
configured provider. Supports --json.

This connects DetectLocalRuntimes, LocalRuntimeCandidates,
DetectedLocalRuntime.SetupAction, ProviderActions, MissingCredentialAction,
SetupCommand, and ProviderState.Actions — a fully built, tested feature that had
no caller. Adds an injectable detectLocalRuntimes appDep for hermetic tests.

* feat(sandbox): add `zero sandbox check` to evaluate a decision as JSON

Wire the previously-unwired zerocommands sandbox-snapshot contract into the CLI.
`zero sandbox check <tool>` evaluates what the sandbox engine would decide for a
hypothetical tool action against the resolved policy, and emits the active plan
(policy + backend + restrictions), the decision (allow/prompt/deny with risk and
any violation), and any persistent grant matching the tool. Supports --json;
grant reasons are redacted.

This connects SandboxPlanSnapshotFromPlan, SandboxPolicySnapshotFromPolicy,
SandboxBackendSnapshotFromBackend, SandboxDecisionSnapshotFromDecision,
SandboxRiskSnapshotFromRisk, SandboxViolationSnapshotFromViolation, and
SandboxGrantMatchSnapshotFromLookup — a fully built, tested JSON contract that
had no production consumer. The existing `sandbox policy` output is unchanged.

* test(providermodelcatalog): cover model predicates and remote fetch layer

Add targeted tests for the audit-identified coverage gaps: the pure logic
(LooksLikeCodingModelID, modelMatchesProvider, defaultedOpenGatewayURL,
modelSortLabel/sortModels) and the HTTP fetch layer (fetchJSON, FetchModelsDev,
FetchOpenGateway, FetchRemote routing) via httptest. Package coverage 48.9% ->
79.5%. cmd/zero (main shim) and browser.OpenURL (exec wrapper) are intentionally
left to CI smoke — their only logic seams (openCommand) are already covered.

* fix(sandbox): address CodeRabbit review on `sandbox check`

- Pass the configured write-root scope (NewScope with AdditionalWriteRoots) into
  the check engine, so its decision matches real enforcement and a stale
  extra-root config surfaces here too instead of a workspace-only fallback.
- Validate --side-effect and --autonomy against their advertised value sets and
  return a usage error on a typo, rather than letting the engine normalize it.
- Reword the unmatched-grant line to "no grant matched this action" — Lookup only
  checks this tool/scope/autonomy, not whether any grant exists for the tool.
- Add tests: invalid-flag rejection and a matched-grant reason-redaction
  regression.

* test(sandbox): make the out-of-workspace check test path OS-portable

Smoke (windows-latest) failed: "/etc/passwd" is not absolute on Windows, so the
check joined it into the workspace and returned "prompt" instead of "deny".
Build the out-of-workspace path under a separate temp dir so it is absolute and
outside the workspace on every OS.
2026-06-18 10:20:24 +05:30

108 lines
3.9 KiB
Go

package cli
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"
"github.com/Gitlawb/zero/internal/config"
"github.com/Gitlawb/zero/internal/provideronboarding"
)
func TestRunProvidersDetectSurfacesLocalRuntimeAndProviderActions(t *testing.T) {
var stdout, stderr bytes.Buffer
deps := commandCenterDeps(t)
deps.detectLocalRuntimes = func(context.Context, provideronboarding.LocalDetectOptions) []provideronboarding.DetectedLocalRuntime {
return []provideronboarding.DetectedLocalRuntime{{
LocalRuntime: provideronboarding.LocalRuntime{CatalogID: "ollama", Name: "Ollama Local", BaseURL: "http://localhost:11434/v1"},
Reachable: true,
Models: []string{"llama3.1"},
}}
}
deps.resolveConfig = func(string, config.Overrides) (config.ResolvedConfig, error) {
// Configured-but-inactive provider with no credential: should yield the full
// "Use provider" / "Check provider" / "Set API key" action set.
profile := config.ProviderProfile{
Name: "openai",
ProviderKind: config.ProviderKindOpenAI,
Model: "gpt-4o",
APIKeyEnv: "OPENAI_API_KEY",
}
return config.ResolvedConfig{ActiveProvider: "other", Providers: []config.ProviderProfile{profile}}, nil
}
exitCode := runWithDeps([]string{"providers", "detect"}, &stdout, &stderr, deps)
if exitCode != exitSuccess {
t.Fatalf("exit=%d stderr=%s", exitCode, stderr.String())
}
out := stdout.String()
for _, want := range []string{
"Detected local runtimes:",
"Ollama Local — http://localhost:11434/v1",
"models: llama3.1",
"zero providers add ollama", // SetupAction's no-key adopt command
"Configured providers:",
"openai",
"Use provider",
"Check provider",
"Set API key",
} {
if !strings.Contains(out, want) {
t.Fatalf("detect output missing %q:\n%s", want, out)
}
}
}
func TestRunProvidersDetectJSONNoRuntimesActiveProvider(t *testing.T) {
var stdout, stderr bytes.Buffer
deps := commandCenterDeps(t)
deps.detectLocalRuntimes = func(context.Context, provideronboarding.LocalDetectOptions) []provideronboarding.DetectedLocalRuntime {
return nil // nothing running locally
}
deps.resolveConfig = func(string, config.Overrides) (config.ResolvedConfig, error) {
// Active provider with an inline key: only "Check provider" remains.
profile := config.ProviderProfile{
Name: "local",
ProviderKind: config.ProviderKindOpenAICompatible,
BaseURL: "https://api.example.com/v1",
APIKey: "sk-secret",
Model: "custom",
}
return config.ResolvedConfig{ActiveProvider: "local", Provider: profile, Providers: []config.ProviderProfile{profile}}, nil
}
exitCode := runWithDeps([]string{"providers", "detect", "--json"}, &stdout, &stderr, deps)
if exitCode != exitSuccess {
t.Fatalf("exit=%d stderr=%s", exitCode, stderr.String())
}
if strings.Contains(stdout.String(), "sk-secret") {
t.Fatalf("detect JSON leaked the API key: %s", stdout.String())
}
var payload struct {
DetectedRuntimes []struct {
CatalogID string `json:"catalogID"`
} `json:"detectedRuntimes"`
Providers []struct {
Name string `json:"name"`
Active bool `json:"active"`
Actions []struct {
Label string `json:"label"`
} `json:"actions"`
} `json:"providers"`
}
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("detect JSON did not decode: %v\n%s", err, stdout.String())
}
if len(payload.DetectedRuntimes) != 0 {
t.Fatalf("expected no detected runtimes, got %#v", payload.DetectedRuntimes)
}
if len(payload.Providers) != 1 || payload.Providers[0].Name != "local" || !payload.Providers[0].Active {
t.Fatalf("expected one active provider 'local', got %#v", payload.Providers)
}
if len(payload.Providers[0].Actions) != 1 || payload.Providers[0].Actions[0].Label != "Check provider" {
t.Fatalf("expected only a Check action for an active keyed provider, got %#v", payload.Providers[0].Actions)
}
}