Skip to content

feat: wire Go CLI to real provider flow#54

Merged
Vasanthdev2004 merged 5 commits into
mainfrom
feat/m0-real-cli-flow
Jun 4, 2026
Merged

feat: wire Go CLI to real provider flow#54
Vasanthdev2004 merged 5 commits into
mainfrom
feat/m0-real-cli-flow

Conversation

@Vasanthdev2004

@Vasanthdev2004 Vasanthdev2004 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • launch the Go Bubble Tea TUI from bare zero instead of showing help
  • wire zero exec to resolved config plus the real OpenAI-compatible provider path
  • add Go M1 headless flags for prompt/file/cwd/model/output/unsafe mode
  • add config path discovery and a provider factory for openai / openai-compatible

Notes

This keeps the scope focused on the real Go CLI flow. It intentionally defers stream-json, sessions, Anthropic/Gemini, MCP/plugins, and model registry UI to follow-up PRs.

The implementation was split with subagents across config/provider factory, TUI bootstrap, and headless exec, then integrated in this branch.

Validation

  • go test ./...
  • bun run typecheck
  • bun test ./tests --timeout 15000 (279 pass)
  • bun run build
  • bun run smoke:build
  • bun run build:go
  • bun run smoke:go

Summary by CodeRabbit

  • New Features

    • Added full exec command with one-shot -p/--prompt, prompt-file support, model/cwd/output-format selection, and unsafe-permissions override
    • Running with no arguments now launches the interactive TUI
    • Improved provider construction and config path resolution (user/project configs + env)
  • Documentation

    • Updated CLI help to document exec, prompt flag, and related flags
  • Tests

    • Expanded CLI, exec, config, and provider factory tests, including JSON streaming and permission behaviors
  • Chores

    • CI review workflow flag adjusted for reviewer signal behavior

@Vasanthdev2004
Vasanthdev2004 marked this pull request as ready for review June 4, 2026 12:21
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a579d96-27c0-4b89-82d6-3db6d4be4ac4

📥 Commits

Reviewing files that changed from the base of the PR and between 96dabde and a901d36.

📒 Files selected for processing (2)
  • internal/cli/app.go
  • internal/cli/app_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/cli/app.go
  • internal/cli/app_test.go

Walkthrough

Adds a new zero exec command with full parsing and JSONL/text streaming; refactors CLI Run to use dependency injection and launch the interactive TUI when no args; adds config path resolution and a provider factory; expands tests across CLI, exec, config, and provider modules.

Changes

CLI exec command and DI refactoring

Layer / File(s) Summary
Config path resolution module
internal/config/paths.go, internal/config/paths_test.go
New helpers to locate optional user and project config files and read ZERO_PROVIDER_COMMAND; validates missing vs directory errors and is tested.
Config resolution precedence and no-provider support
internal/config/resolver.go, internal/config/resolver_test.go
Apply env overrides before provider-command, stop pre-setting ActiveProvider, allow empty providers list, and add tests for provider-command overrides, no-provider success, and misconfigured active provider.
Provider construction factory
internal/providers/factory.go, internal/providers/factory_test.go
Factory creates OpenAI/OpenAI-compatible providers with injected HTTP client and UserAgent options; rejects unsupported kinds; tests verify request headers and streaming iteration.
CLI entrypoint refactoring with DI and TUI launch
internal/cli/app.go, internal/cli/app_test.go
Run delegates through appDeps; no-args now launches the TUI; adds -p/--prompt routing to exec; builds core registry and provider for TUI; updates help and exec messaging; tests cover TUI launch, provider injection, and config error handling.
Exec command implementation
internal/cli/exec.go, internal/cli/exec_test.go
Implements zero exec parsing (prompt/file/model/cwd/output-format/skip-permissions-unsafe), workspace & prompt resolution, pre-runtime validation, provider resolution and permission mode selection, and streaming output via JSONL or text; extensive tests for help, prompt composition, validation, unsafe mode, JSON events, and provider integration.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI.Run/runExec
  participant Parser as parseExecArgs
  participant Workspace as resolveWorkspaceRoot
  participant Prompt as resolveExecPrompt
  participant Config as resolveConfig
  participant Factory as providers.New
  participant Agent as agent.Run
  participant Writer as execEventWriter

  CLI->>Parser: parse flags & args
  Parser->>Workspace: resolve --cwd
  Parser->>Prompt: build prompt (inline + file)
  CLI->>Config: resolveConfig(workspace)
  Config-->>CLI: provider profile
  CLI->>Factory: New(profile, Options)
  Factory-->>CLI: provider
  CLI->>Agent: Run(prompt, registry, provider)
  Agent->>Writer: stream completion events
  Writer-->>CLI: stdout/stderr or NDJSON
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Gitlawb/zero#51: Both PRs change the zero exec CLI wiring; #51 originally added an exec handler and offlineProvider that this PR refactors/moves.
  • Gitlawb/zero#48: Related to permission/unsafe-mode execution and tool registry gating referenced by the new exec/TUI permission handling.
  • Gitlawb/zero#52: Related edits to runtime/provider/TUI foundations used by the new exec event-writer and tests.

Suggested reviewers

  • anandh8x
  • gnanam1990
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: wire Go CLI to real provider flow' clearly and specifically describes the main change—integrating the Go CLI with real provider execution and configuration resolution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/m0-real-cli-flow

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
internal/config/resolver_test.go (1)

169-187: ⚡ Quick win

Add a regression case for active-provider-without-profiles.

The new test at Line 170 only covers fully empty config. Please also cover activeProvider set with zero providers, so misconfiguration handling stays explicit.

Suggested test
 func TestResolveAllowsNoConfiguredProviders(t *testing.T) {
@@
 }
+
+func TestResolveRejectsActiveProviderWithoutConfiguredProfiles(t *testing.T) {
+	path := writeConfig(t, `{"activeProvider":"ghost","providers":[]}`)
+
+	_, err := Resolve(ResolveOptions{ProjectConfigPath: path, Env: map[string]string{}})
+	if err == nil {
+		t.Fatal("Resolve() error = nil, want missing active provider error")
+	}
+	if !strings.Contains(err.Error(), `active provider "ghost" not found`) {
+		t.Fatalf("error = %q, want active provider missing message", err.Error())
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/resolver_test.go` around lines 169 - 187, Add a new unit test
(e.g., TestResolveActiveProviderWithoutProfiles) that calls
Resolve(ResolveOptions{Env: map[string]string{"ACTIVE_PROVIDER": "foo"}}) with
no configured providers and asserts that Resolve returns an error (or at least
does not set a valid active provider) and that resolved.ActiveProvider is empty,
resolved.Providers is empty, resolved.Provider equals the zero-value
ProviderProfile, and resolved.MaxTurns equals defaultMaxTurns; reference the
Resolve function, ResolveOptions type, ActiveProvider field, ProviderProfile
type, and defaultMaxTurns in the test to locate and validate the behavior.
internal/cli/exec_test.go (1)

20-43: ⚡ Quick win

Add a regression test for exec --help combined with extra args.

Please add a case like zero exec --help --model m1 asserting help output and no runtime/provider execution. This protects the parser behavior from silently regressing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/exec_test.go` around lines 20 - 43, Add a regression case to
TestRunExecHelpDocumentsM1Flags that calls Run with extra args (e.g.,
Run([]string{"exec","--help","--model","m1"}, &stdout, &stderr)) and asserts the
same behavior as the existing help-only case: exit code 0, stdout contains the
help flags (-f/--file, -m/--model, etc.), and stderr is empty; use the same
stdout/stderr buffers and checks around Run (the Run function is the CLI entry
used here) so the parser shows help without invoking runtime/provider execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/app.go`:
- Around line 187-198: Update the hardcoded usage string printed in the exec
command help (the fmt.Fprint call in internal/cli/app.go) to reflect that the
prompt is optional and that input can come from --file or --prompt flags; change
the usage line from "zero exec [flags] <prompt>" to something like "zero exec
[flags] [<prompt>]" or "zero exec [flags] [prompt]" and add/mention the --prompt
flag in the Flags section alongside -f, --file so the help matches actual parser
behavior in the exec command implementation.

In `@internal/cli/exec.go`:
- Around line 98-114: The pre-run writer.runStart call can set writer.err (e.g.,
broken stdout/stderr) but the code proceeds to call agent.Run; detect writer.err
immediately after writer.runStart (and after writer.warning if
options.skipPermissionsUnsafe) and short-circuit by returning exitCrash instead
of invoking agent.Run. Update the control flow around writer.runStart /
options.skipPermissionsUnsafe to check writer.err and return early, referencing
writer.runStart, writer.err, and agent.Run to locate the change.
- Around line 135-140: The help flag currently only returns when it's the sole
arg, causing "exec --help --model x" to be treated as a prompt; change the
help-handling branch in the argument parsing switch (the case handling arg ==
"-h" || arg == "--help" || arg == "help") to treat any occurrence of a help flag
as a request for help: immediately return options, true, nil (or set a dedicated
showHelp flag on options and return after parsing) instead of appending to
options.promptParts; update references in the same parsing function where
options.promptParts is modified so help never falls through to execute provider
calls.
- Around line 287-299: The JSON branch in writeExecProviderError ignores errors
from writeJSONLine and always returns exitProvider; change it to capture and
check the error returned by each writeJSONLine call (the two calls that write
the "error" and "done" JSON objects) and if any write returns a non-nil error,
return exitCrash instead of exitProvider; otherwise, keep returning
exitProvider. Ensure you reference writeExecProviderError, writeJSONLine,
exitProvider and exitCrash when applying the change.

In `@internal/config/paths.go`:
- Line 30: Trim whitespace from the ZERO_PROVIDER_COMMAND environment variable
before storing it in the configuration so whitespace-only values become empty;
update the assignment for ProviderCommand (the struct field) to use
strings.TrimSpace(os.Getenv("ZERO_PROVIDER_COMMAND")) so later logic treating
empty vs non-empty commands behaves correctly.

In `@internal/config/resolver.go`:
- Around line 183-185: The current code returns success when providers is empty
regardless of activeName, which hides config errors; update the providers-empty
branch to check if activeName (or activeProvider) is set and, if so, return an
error instead of nil success; specifically modify the block that returns
[]ProviderProfile{}, ProviderProfile{}, nil to return a descriptive error when
activeName != "" (otherwise keep returning the empty slice and zero-value
ProviderProfile with nil error) so callers can surface misconfigured active
provider names.

---

Nitpick comments:
In `@internal/cli/exec_test.go`:
- Around line 20-43: Add a regression case to TestRunExecHelpDocumentsM1Flags
that calls Run with extra args (e.g.,
Run([]string{"exec","--help","--model","m1"}, &stdout, &stderr)) and asserts the
same behavior as the existing help-only case: exit code 0, stdout contains the
help flags (-f/--file, -m/--model, etc.), and stderr is empty; use the same
stdout/stderr buffers and checks around Run (the Run function is the CLI entry
used here) so the parser shows help without invoking runtime/provider execution.

In `@internal/config/resolver_test.go`:
- Around line 169-187: Add a new unit test (e.g.,
TestResolveActiveProviderWithoutProfiles) that calls Resolve(ResolveOptions{Env:
map[string]string{"ACTIVE_PROVIDER": "foo"}}) with no configured providers and
asserts that Resolve returns an error (or at least does not set a valid active
provider) and that resolved.ActiveProvider is empty, resolved.Providers is
empty, resolved.Provider equals the zero-value ProviderProfile, and
resolved.MaxTurns equals defaultMaxTurns; reference the Resolve function,
ResolveOptions type, ActiveProvider field, ProviderProfile type, and
defaultMaxTurns in the test to locate and validate the behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d6341af-0c4d-4a42-abb7-9ab7ecda0629

📥 Commits

Reviewing files that changed from the base of the PR and between e9b3d7b and 83f7b8d.

📒 Files selected for processing (10)
  • internal/cli/app.go
  • internal/cli/app_test.go
  • internal/cli/exec.go
  • internal/cli/exec_test.go
  • internal/config/paths.go
  • internal/config/paths_test.go
  • internal/config/resolver.go
  • internal/config/resolver_test.go
  • internal/providers/factory.go
  • internal/providers/factory_test.go

Comment thread internal/cli/app.go
Comment thread internal/cli/exec.go
Comment thread internal/cli/exec.go
Comment thread internal/cli/exec.go
Comment thread internal/config/paths.go Outdated
Comment thread internal/config/resolver.go
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Typecheck: bun run typecheck
  • [pass] Tests: bun run test
  • [pass] Build: bun run build
  • [pass] Smoke build: bun run smoke:build

Scope

Head: a901d3696fe7
Changed files (11): .coderabbit.yaml, internal/cli/app.go, internal/cli/app_test.go, internal/cli/exec.go, internal/cli/exec_test.go, internal/config/paths.go, internal/config/paths_test.go, internal/config/resolver.go, internal/config/resolver_test.go, internal/providers/factory.go, internal/providers/factory_test.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rechecked current head 83f7b8d9870b2b84aee95fc7a295c6aab85e7b89 locally. The Go test suite is green, but there are still runtime/config blockers that should be fixed before merge.

Findings:

  • internal/cli/exec.go:135: zero exec --help --model test is still parsed as a prompt because help only short-circuits when it is the only exec arg. I reproduced this with a direct CLI probe: instead of printing help, it continued into provider setup and failed with a provider/auth error. Help should win whenever -h/--help is present in the exec args, even with other flags.
  • internal/config/resolver.go:182: an explicit active provider with no provider profiles is silently accepted as an empty provider config. For example, ZERO_PROVIDER=ghost with no matching provider ends up as the generic “No provider configured” path instead of reporting that the active provider is invalid/missing. If activeName is non-empty and providers is empty, normalizeProviders should return an error.
  • internal/config/paths.go:30: ZERO_PROVIDER_COMMAND is passed through without trimming. With ZERO_PROVIDER_COMMAND=' ', the resolver attempts to execute a blank command and surfaces provider command returned empty JSON. Trim the env value before assigning/using it so whitespace-only values behave like unset.
  • internal/cli/exec.go:98: startup JSON/warning write failures are not checked until after agent.Run, so a broken/unwritable output stream can still trigger the provider call and spend tokens before the crash is returned. Check writer.err immediately after runStart/warning and before entering the agent loop.
  • internal/cli/exec.go:287: the JSON provider-error path ignores writeJSONLine failures and still returns exitProvider. If either JSON error/done write fails, this should return exitCrash, consistent with the rest of the writer error handling.

Validation run:

  • go test ./internal/cli ./internal/config ./internal/providers passed
  • go test ./... passed
  • git diff --check origin/main...HEAD passed
  • Direct CLI probes reproduced the help parsing and whitespace provider-command issues above

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/exec_test.go (1)

377-379: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid panic-prone payload assertions in test validation.

Line 377 and Line 378 can panic on payload shape changes; prefer checked assertions so failures stay deterministic and readable.

Proposed patch
-	messages := gotBody["messages"].([]any)
-	lastMessage := messages[len(messages)-1].(map[string]any)
-	if lastMessage["content"] != "hello provider" {
+	messages, ok := gotBody["messages"].([]any)
+	if !ok || len(messages) == 0 {
+		t.Fatalf("messages = %#v, want non-empty []any", gotBody["messages"])
+	}
+	lastMessage, ok := messages[len(messages)-1].(map[string]any)
+	if !ok {
+		t.Fatalf("last message = %#v, want map[string]any", messages[len(messages)-1])
+	}
+	if lastMessage["content"] != "hello provider" {
 		t.Fatalf("last provider message = %#v, want prompt", lastMessage)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/exec_test.go` around lines 377 - 379, The test currently uses
bare type assertions on gotBody["messages"] and lastMessage which can panic if
the JSON shape changes; replace them with safe, checked assertions (e.g., use
the "ok" comma-ok form or testing helpers like require/ assert) to verify that
gotBody["messages"] is a []any, that len(messages) > 0, and that the last
element is a map[string]any before accessing "content"; then assert the content
equals "hello provider" with a descriptive failure message. Target the
variables/messages handling in exec_test.go (gotBody, messages, lastMessage) and
change the assertions to fail deterministically rather than panic.
🧹 Nitpick comments (1)
internal/cli/exec_test.go (1)

334-341: ⚡ Quick win

Assert request method and endpoint path in the provider integration test.

This handler currently accepts any route/method, so a request-routing regression can still pass this test. Add explicit assertions for method and path suffix.

Proposed patch
-	var gotAuth string
+	var gotAuth string
+	var gotMethod string
+	var gotPath string
 	var gotBody map[string]any
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		gotMethod = r.Method
+		gotPath = r.URL.Path
 		gotAuth = r.Header.Get("Authorization")
 		if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
 			t.Fatalf("decode provider request: %v", err)
 		}
 		_, _ = io.WriteString(w, "data: {\"choices\":[{\"delta\":{\"content\":\"provider ok\"}}]}\n\n")
 		_, _ = io.WriteString(w, "data: [DONE]\n\n")
 	}))
@@
 	if gotAuth != "Bearer sk-local" {
 		t.Fatalf("Authorization = %q, want project config token", gotAuth)
 	}
+	if gotMethod != http.MethodPost {
+		t.Fatalf("method = %q, want %q", gotMethod, http.MethodPost)
+	}
+	if !strings.HasSuffix(gotPath, "/chat/completions") {
+		t.Fatalf("path = %q, want suffix /chat/completions", gotPath)
+	}

Also applies to: 371-373

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/exec_test.go` around lines 334 - 341, The test HTTP handler used
in exec_test.go accepts any method/path—add assertions inside the httptest
server handler to check r.Method equals the expected method (e.g., "POST") and
that r.URL.Path or r.URL.String() has the expected suffix (the provider
endpoint) before decoding the body; update both occurrences (the handler around
gotAuth/gotBody at the shown snippet and the similar handler at lines ~371-373)
to fail the test (t.Fatalf) when method or path mismatches so request-routing
regressions are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/cli/exec_test.go`:
- Around line 377-379: The test currently uses bare type assertions on
gotBody["messages"] and lastMessage which can panic if the JSON shape changes;
replace them with safe, checked assertions (e.g., use the "ok" comma-ok form or
testing helpers like require/ assert) to verify that gotBody["messages"] is a
[]any, that len(messages) > 0, and that the last element is a map[string]any
before accessing "content"; then assert the content equals "hello provider" with
a descriptive failure message. Target the variables/messages handling in
exec_test.go (gotBody, messages, lastMessage) and change the assertions to fail
deterministically rather than panic.

---

Nitpick comments:
In `@internal/cli/exec_test.go`:
- Around line 334-341: The test HTTP handler used in exec_test.go accepts any
method/path—add assertions inside the httptest server handler to check r.Method
equals the expected method (e.g., "POST") and that r.URL.Path or r.URL.String()
has the expected suffix (the provider endpoint) before decoding the body; update
both occurrences (the handler around gotAuth/gotBody at the shown snippet and
the similar handler at lines ~371-373) to fail the test (t.Fatalf) when method
or path mismatches so request-routing regressions are caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b2136131-e895-4eb7-948b-21ab783c3447

📥 Commits

Reviewing files that changed from the base of the PR and between 83f7b8d and 232b0bb.

📒 Files selected for processing (7)
  • internal/cli/app.go
  • internal/cli/exec.go
  • internal/cli/exec_test.go
  • internal/config/paths.go
  • internal/config/paths_test.go
  • internal/config/resolver.go
  • internal/config/resolver_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/config/paths_test.go
  • internal/config/resolver_test.go
  • internal/cli/exec.go
  • internal/config/paths.go
  • internal/cli/app.go

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved current head 608c1d38a8649405e78af9867fa72e57c8c95207.

I rechecked the requested-change items from my prior review. The exec help path no longer falls through to provider execution, pre-run writer errors now short-circuit before agent.Run, JSON provider-error writes return exitCrash on I/O failure, ZERO_PROVIDER_COMMAND is trimmed, active-provider-without-profiles now errors explicitly, and exec help text matches optional prompt/--prompt behavior. CodeRabbit has no new actionable comments and CI is green.

Validation run:

  • git diff --check origin/main...HEAD
  • go test ./internal/cli ./internal/config ./internal/providers
  • go test ./...
  • env -u OPENAI_API_KEY -u OPENAI_MODEL -u OPENAI_BASE_URL -u ZERO_PROVIDER -u ZERO_PROVIDER_COMMAND go run ./cmd/zero exec --help --model test
  • XDG_CONFIG_HOME=/private/tmp/zero-pr54-empty-config ZERO_PROVIDER=ghost go run ./cmd/zero exec hello confirmed explicit missing active-provider error
  • XDG_CONFIG_HOME=/private/tmp/zero-pr54-blank-command-config ZERO_PROVIDER_COMMAND=" " go run ./cmd/zero exec hello confirmed whitespace provider command behaves as unset
  • PATH=/Users/kratos/.bun/bin:$PATH bun install --frozen-lockfile
  • PATH=/Users/kratos/.bun/bin:$PATH bun run typecheck
  • PATH=/Users/kratos/.bun/bin:$PATH bun test ./tests --timeout 15000 (279 pass / 0 fail)
  • PATH=/Users/kratos/.bun/bin:$PATH bun run build
  • PATH=/Users/kratos/.bun/bin:$PATH bun run smoke:build
  • PATH=/Users/kratos/.bun/bin:$PATH bun run build:go
  • PATH=/Users/kratos/.bun/bin:$PATH bun run smoke:go

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's good

  • appDeps is a clean DI seam for the CLI. appDeps{ getwd, resolveConfig, newProvider, runTUI } (internal/cli/app.go:25-31) plus fillAppDeps (internal/cli/app.go:104-119) means every test can swap in a fake for any subset of the four collaborators without reaching for monkey-patching. The test surface — TestRunNoArgsLaunchesTUIWithNilProviderWhenNoProviderConfigured, TestRunNoArgsLaunchesTUIWithResolvedProviderMetadata, TestRunNoArgsReportsConfigErrorsWithoutLaunchingTUI, TestRunCommandsDoNotLaunchTUI — exercises all four branches and proves the seam doesn't leak (TUI is only invoked when buildProvider resolves, the right ProviderName/ModelName flow into tui.Options, and assertCoreRegistry confirms the 9-tool core set is wired).
  • zero with no args launches the TUI, zero exec and zero -p route to the same headless runner. The case "-p" | "--prompt" handler in app.go:72-78 rewrites into execArgs and delegates to runExec, so there's exactly one prompt-parser / provider-construction path. TestRunPromptFlagRoutesToExecRunner (internal/cli/exec_test.go:185-208) verifies the stdout/stderr/exit-code are bit-identical between exec hello zero and -p hello zero.
  • parseExecArgs handles every flag form the help text advertises. -f/--file, -m/--model, -C/--cwd, -o/--output-format text|json, --prompt, --skip-permissions-unsafe, and the -- terminator that flushes the rest as positional prompt parts (internal/cli/exec.go:81-152). Both --flag value and --flag=value are supported. The -- terminator is a nice touch for zero exec -- "--file foo.txt" style invocations.
  • Text and JSON output share the same callback set with no logic duplication. execEventWriter (internal/cli/exec.go:155-269) routes text to stdout deltas in text mode and to {"type":"text","delta":...} lines in JSON mode; tool calls/results/usage get a symmetric split. The error sticky-flag (writer.err is set on first write failure, all subsequent writes are no-ops) means a closed pipe can't crash mid-stream — TestRunExecJSONRunStartWriteFailureSkipsAgent and TestRunExecJSONProviderErrorWriteFailureReturnsCrash cover both the early (run_start) and late (provider_error) write-failure paths.
  • resolveWorkspaceRoot and resolveExecPrompt do real validation before runtime. --cwd rejects non-existent paths and files-not-directories with exit 2 (exec.go:172-187, tested by TestRunExecRejectsInvalidCwdBeforeRuntime). --file rejects missing/empty files with a clear error. The agent runtime is never invoked when these validations fail — verified by the strings.Contains(... "Go agent runtime ready") negative-assertion in both tests.
  • paths.go adds proper os.UserConfigDir() integration with platform-aware test setup. DefaultResolveOptions(workspaceRoot) (internal/config/paths.go:18-44) returns the user config path ($XDG_CONFIG_HOME/zero/config.json on Linux, $HOME/Library/Application Support/zero/config.json on macOS, %APPDATA%/zero/config.json on Windows) plus the project-level $workspaceRoot/.zero/config.json, and setUserConfigRoot (paths_test.go:140-158) overrides the right env var per runtime.GOOS so the same test runs on all three platforms.
  • Resolver change reorders env vs. provider-command correctly. applyEnv now runs before LoadProviderCommand (internal/config/resolver.go:21-37), so a provider command can override env-set keys — TestResolveProviderCommandOverridesEnvProviderFields proves OPENAI_API_KEY=sk-env + command apiKey=sk-commandsk-command wins. The default-ActiveProvider change (removed the implicit openai) is the right semantic: zero-config should mean "no provider", not "synthetic OpenAI profile".
  • Provider factory is a single switch, ready for Anthropic/Gemini adapters to drop in. providers.New (internal/providers/factory.go:30-46) currently routes both openai and openai-compatible to openai.New and returns unsupported provider kind for anything else. The factory options struct carries UserAgent (zero/<version>) and a *http.Client for test injection — the TestNewCreatesOpenAIProviderWithFactoryOptions test uses a captureTransport to assert the request URL, Authorization header, and User-Agent header all flow through.
  • End-to-end exec test against a real httptest.Server is the right shape. TestRunExecUsesProjectConfigAndOpenAICompatibleProvider (exec_test.go:425-520) writes a .zero/config.json with provider_kind: openai-compatible, runs Run(["exec", "--cwd", root, "hello provider"]), and asserts: exit 0, stdout equals the SSE-decoded provider ok, Authorization: Bearer sk-local, POST /v1/chat/completions, model: local-model, and the user prompt is the last message. This catches the most common wiring regression (auth header, model name, message ordering) in one shot.
  • Exit-code differentiation is solid. exitSuccess=0, exitCrash=1, exitUsage=2, exitProvider=3 (exec.go:14-19). Provider failures get code 3 in both text and JSON mode (with {"type":"done","exit_code":3} in JSON), which lets shell scripts distinguish "no provider configured" from "usage error" from "agent crashed".

Observations (non-blocking)

  1. nextFlagValue doesn't reject flag-shaped values. internal/cli/exec.go:158-164 returns the next arg as the value as long as it's not empty, even if it starts with -. So zero exec --prompt --model gpt-4 parses as prompt = "--model", then gpt-4 becomes a positional prompt part, giving the user "--model gpt-4" as the prompt with no error. The same gap was closed in scripts/build.ts from #53 (parseBuildArgs rejects flag-shaped values via requireValue); the same one-liner fix would close it here. Worth a follow-up test like parseBuildArgs(['-o', '-h']) from #53's tests/build-scripts.test.ts:88-91.

  2. No --ask mode for zero exec. agent.PermissionMode has three values (auto, ask, unsafe per internal/agent/types.go:14-19), but the CLI exposes only --skip-permissions-unsafe to flip the default auto into unsafe. The middle tier (ask) — which would be the right default for an interactive user typing prompts into a CLI one-shot — is unreachable. A --permission-mode auto|ask|unsafe flag (or a ZERO_PERMISSION_MODE env) would be the natural complement to the existing config plumbing.

  3. buildProvider's (nil, nil) "no provider" sentinel is asymmetric with runExec. The TUI accepts Provider: nil and is expected to surface a "no provider configured" panel; runExec returns exitProvider=3 with the message "No provider configured. Set OPENAI_MODEL/OPENAI_API_KEY or add .zero/config.json.". A reader of app.go has to follow the control flow to discover which callers tolerate nil. A typed OptionalProvider (or an explicit provider.IsZero() predicate on ProviderProfile) would make the intent self-documenting.

  4. fillAppDeps silently falls back to production implementations. If a test sets only runTUI and resolveConfig, the other three fields are filled with os.Getwd, config.Resolve (which reads the filesystem and may shell out), and providers.New (which calls openai.New). The current tests avoid this by always supplying all four deps, but a future test that forgets one would silently hit real I/O. A test that injects only runTUI and expects an error would surprise the next reader. Either document the pattern ("always inject all four deps") or use a constructor that requires all four.

  5. The resolver's removed default-ActiveProvider is a silent behavior change. Old code: ActiveProvider: string(ProviderKindOpenAI) meant an empty cfg resolved to a synthetic OpenAI profile built from OPENAI_API_KEY/OPENAI_MODEL. New code: empty cfg returns ResolvedConfig{} and the runner reports "no provider configured". This is the right change (no more implicit OpenAI assumption), but any user with env-only config (no ~/.config/zero/config.json or .zero/config.json) and who upgraded silently loses their setup. The PR body is honest about the scope but doesn't call this out — a one-line note in the PR description or a CHANGELOG entry would prevent a support churn spike.

  6. --prompt at the top level uses a string-append hack. execArgs := append([]string{"--prompt", args[1]}, args[2:]...) (app.go:75-76) is fine but the call then re-enters runExec with args[1:] = execArgs, which means the help text path inside runExec won't see "exec" as args[0] (it sees "--prompt" instead). It works because parseExecArgs doesn't check args[0], but it would be cleaner to either (a) handle the flag inline in runWithDeps and build the execOptions directly, or (b) make the top-level --prompt route through the same parseExecArgs by reconstructing args to start with exec. Functionally identical, just a code-shape concern.

  7. resolveExecPrompt joins inline and file prompts with \n\n even though the help says "Read prompt text from a file". The help line -f, --file <path> Read prompt text from a file (app.go:179-180) reads as "this is the prompt", but resolveExecPrompt (exec.go:190-221) concatenates inline + "\n\n" + file when both are provided. TestRunExecAssemblesInlineAndFilePromptRelativeToCwd documents the join, but the help text doesn't. Either change the help to say "Append prompt text from a file" or document the behavior with a short example in the help output.

  8. truncateForStatus produces a 203-character string but the limit is 200. exec.go:294-300 builds a 200-char compact string then appends "...", giving 203 chars total. Trivial, but if the result is fed into a fixed-width terminal or a downstream column-width check, the 3-char overflow could misalign formatting. Either drop the prefix to 197 or rename the constant to make the intent clear.

  9. JSON event type naming is snake_case; zeroruntime stream events are kebab-case. {"type":"run_start",...}, {"type":"tool_call",...}, {"type":"tool_result",...} (exec.go:218-264) are snake_case; PR #52's zeroruntime contract uses tool-call-start / tool-call-delta / tool-call-end (kebab-case). These are different APIs (CLI exec output vs. agent stream), so the inconsistency isn't wrong, but anyone reading both will trip on it. Worth a one-line comment in the event-emitter code explaining the two namespaces.

  10. No test for what happens when the OpenAI provider returns a real 4xx/5xx. TestRunExecJSONProviderErrorWriteFailureReturnsCrash (exec_test.go:151-173) tests the case where stderr write fails after a config error. There's no test where the provider itself returns an error (e.g. 401 from the httptest.Server). A TestRunExecSurfacesProviderAPIError that has the mock SSE server return 401 Unauthorized and asserts the user sees a clear provider_error event + exit code 3 would close the gap.

  11. runExec doesn't surface OnUsage in text mode, but in JSON mode the usage event is emitted after the final text delta — which means a long stream may not get usage until after the user has already seen the answer. The order is run_starttext*finaldone per TestRunExecJSONOutputsNDJSONEvents. If the provider sends usage mid-stream (some do, for prompt-cache prefill accounting), the current code buffers it. The contract isn't wrong, but a docstring on execEventWriter.usage would help the next reader understand the timing.

  12. appDeps.runTUI receives a context.Background(). runInteractiveTUI (app.go:122-165) and runExec (exec.go:43-176) both pass context.Background() to deps.runTUI/agent.Run. There's no way for a caller (smoke test, CI, wrapper script) to wire in a cancellation context or deadline. A context.Context parameter on Run (with context.Background() as the default in cmd/zero/main.go) would let downstream tests exercise cancellation.

  13. --model override doesn't go through the model registry. The flag sets overrides.Provider.Model (exec.go:68-72), which the resolver then merges. But the registry (#56) has DefaultModelID = "gpt-4.1" and a Require(pattern) that validates against known models + aliases. If the user passes --model typo-of-4-1, the resolver accepts it, the provider tries to send it to the API, and the API 4xxs. A registry lookup in runExec (or in the resolver's applyOverrides) would catch this earlier with a friendlier error.

  14. paths_test.go doesn't test the directory-rejection path under the macOS or Windows env override branches. TestDefaultResolveOptionsRejectsDirectoryConfigPaths uses t.Run to parameterize by user/project, but the setUserConfigRoot helper covers all three platforms. A test that sets HOME on darwin + a directory at $HOME/zero/config.json and asserts the same error message would lock in the platform behavior beyond the runtime.GOOS switch.

  15. runInteractiveTUI exits 0 on successful TUI return, but there's no signal that the TUI ran vs. immediately bailed. The test TestRunNoArgsLaunchesTUIWithResolvedProviderMetadata returns 0 for a successful TUI and the TestRunCommandsDoNotLaunchTUI test returns whatever runTUI returned (default 0). If the TUI panics inside its Bubble Tea loop, the test harness would surface it as a Go panic but the CLI exit code would be 1 (Go's default panic exit). A defer recover() wrapper around deps.runTUI would convert TUI panics into a clean error report. Minor.

No blockers. The appDeps DI seam, the unified prompt-routing path, the resolver-rewrite with the right env/command ordering, the JSON/text split in execEventWriter, and the end-to-end httptest.Server test are all genuine upgrades to the M1 Go path. The nextFlagValue flag-shaped-value gap (#1) is the only one I'd want to see fixed in this PR — it's a one-liner and matches the pattern already established in #53's scripts/build.ts. The rest are polish items for follow-up PRs (--ask mode, registry-aware model validation, TUI context plumbing, error-panic recovery).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants