From 2898fed8fa823d97781c5fbfa502cbb4ab896078 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Wed, 14 Oct 2026 11:12:40 +0800 Subject: [PATCH] feat(cli): add agent command with test fixture support This change introduces a new agent command to the CLI along with its test coverage. The main additions are grouped below. **New agent command:** * Added `internal/cli/agent_command.go` implementing the agent command capability for users. * Added `internal/cli/agent_command_test.go` with tests covering the new agent command behaviour. **Test fixture wiring:** * Updated `TestMain` in `internal/cli/mcp_process_test.go` to dispatch to `runAgentCommandFixture` when invoked with the `__jevwise-client-fixture` argument, enabling the agent command tests to exercise a synthetic client in an isolated subprocess. --- internal/cli/agent_command.go | 59 +++++++++++++++++ internal/cli/agent_command_test.go | 101 +++++++++++++++++++++++++++++ internal/cli/mcp_process_test.go | 3 + 3 files changed, 163 insertions(+) create mode 100644 internal/cli/agent_command.go create mode 100644 internal/cli/agent_command_test.go diff --git a/internal/cli/agent_command.go b/internal/cli/agent_command.go new file mode 100644 index 0000000..e6c5cad --- /dev/null +++ b/internal/cli/agent_command.go @@ -0,0 +1,59 @@ +package cli + +import ( + "context" + "errors" + "io" + "os" + "os/exec" + "time" +) + +type agentCommand func(context.Context, string, ...string) ([]byte, error) + +const agentOutputLimit = 1 << 20 + +type agentOutput struct { + data []byte + full bool +} + +func (w *agentOutput) Write(p []byte) (int, error) { + n := min(len(p), agentOutputLimit-len(w.data)) + w.data = append(w.data, p[:n]...) + w.full = w.full || n != len(p) + return len(p), nil +} + +// Native clients own their settings edits. No shell, raw diagnostics or inherited +// credentials; only platform paths and client configuration locations are passed. +func runAgentCommand(ctx context.Context, client string, args ...string) ([]byte, error) { + if client != "codex" && client != "claude" { + return nil, errors.New("unsupported MCP client") + } + process := exec.CommandContext(ctx, client, args...) + process.WaitDelay = 250 * time.Millisecond + process.Env = agentEnvironment() + var output agentOutput + process.Stdout, process.Stderr = &output, io.Discard + if err := process.Run(); err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + return nil, err + } + if output.full { + return nil, errors.New("client output exceeds limit") + } + return output.data, nil +} + +func agentEnvironment() []string { + var env []string + for _, name := range []string{"PATH", "PATHEXT", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "LOCALAPPDATA", "APPDATA", "TEMP", "TMP", "TMPDIR", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME"} { + if value, ok := os.LookupEnv(name); ok { + env = append(env, name+"="+value) + } + } + return append(env, "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1", "DISABLE_AUTOUPDATER=1", "DISABLE_TELEMETRY=1", "DISABLE_ERROR_REPORTING=1") +} diff --git a/internal/cli/agent_command_test.go b/internal/cli/agent_command_test.go new file mode 100644 index 0000000..b6da1e1 --- /dev/null +++ b/internal/cli/agent_command_test.go @@ -0,0 +1,101 @@ +package cli + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +func TestAgentCommandEnvironmentAndOutputAreBounded(t *testing.T) { + for _, name := range []string{"JEV_API_KEY", "JEVWISE_MCP_TOKEN", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MCP_CLIENT_SECRET", "UNRELATED_SECRET"} { + t.Setenv(name, "private-secret") + } + t.Setenv("CODEX_HOME", "client-config-fixture") + env := strings.Join(agentEnvironment(), "\n") + if strings.Contains(env, "private") || !strings.Contains(env, "CODEX_HOME=client-config-fixture") || !strings.Contains(env, "DISABLE_AUTOUPDATER=1") { + t.Fatal("registration child inherited credentials or lost configuration location") + } + var output agentOutput + data := []byte(strings.Repeat("x", agentOutputLimit+1)) + if n, err := output.Write(data); err != nil || n != len(data) || len(output.data) != agentOutputLimit || !output.full { + t.Fatal("client output was not bounded") + } + _, _ = output.Write(data) + if len(output.data) != agentOutputLimit { + t.Fatal("overflowing output kept growing") + } + if _, err := runAgentCommand(context.Background(), "unsupported-client"); err == nil { + t.Fatal("unsupported client executable was launched") + } +} + +// Only the task-owned test binary, installed under a temporary client name, +// enters this fixture. No installed agent or user configuration is accessed. +func runAgentCommandFixture() int { + switch os.Args[2] { + case "timeout": + time.Sleep(time.Hour) + case "large": + fmt.Print(strings.Repeat("x", agentOutputLimit+1)) + default: + for _, name := range []string{"JEV_API_KEY", "JEVWISE_MCP_TOKEN", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"} { + if os.Getenv(name) != "" { + return 2 + } + } + if os.Getenv("CODEX_HOME") != "isolated-client-fixture" { + return 3 + } + fmt.Fprintln(os.Stderr, "private-client-diagnostic") + fmt.Print("clean") + } + return 0 +} + +func TestAgentCommandRunsOnlyIsolatedFixtureWithDeadline(t *testing.T) { + binary, err := os.Executable() + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + name := "codex" + if runtime.GOOS == "windows" { + name += ".exe" + } + path := filepath.Join(dir, name) + if err := os.Link(binary, path); err != nil { + data, err := os.ReadFile(binary) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0755); err != nil { + t.Fatal(err) + } + } + t.Setenv("PATH", dir) + t.Setenv("CODEX_HOME", "isolated-client-fixture") + for _, key := range []string{"JEV_API_KEY", "JEVWISE_MCP_TOKEN", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"} { + t.Setenv(key, "private-fixture-key") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + data, err := runAgentCommand(ctx, "codex", "__jevwise-client-fixture", "clean") + if err != nil || string(data) != "clean" { + t.Fatal("child inherited credentials or diagnostics", err) + } + if data, err := runAgentCommand(ctx, "codex", "__jevwise-client-fixture", "large"); err == nil || data != nil { + t.Fatal("oversized subprocess output was accepted") + } + ctx, stop := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer stop() + started := time.Now() + if _, err := runAgentCommand(ctx, "codex", "__jevwise-client-fixture", "timeout"); !errors.Is(err, context.DeadlineExceeded) || time.Since(started) > 5*time.Second { + t.Fatal("owned client process ignored deadline", err) + } +} diff --git a/internal/cli/mcp_process_test.go b/internal/cli/mcp_process_test.go index 8e95421..03bc1dd 100644 --- a/internal/cli/mcp_process_test.go +++ b/internal/cli/mcp_process_test.go @@ -23,6 +23,9 @@ import ( // Only the task-owned test binary gets this synthetic child service. Production // CLI registration, bootstrap, privacy, HTTP and cleanup remain real code paths. func TestMain(m *testing.M) { + if len(os.Args) == 3 && os.Args[1] == "__jevwise-client-fixture" { + os.Exit(runAgentCommandFixture()) + } if len(os.Args) == 2 && os.Args[1] == daemon.ChildArgument && os.Getenv("JEVWISE_PROCESS_FIXTURE") == "1" { os.Exit(runProcessFixture()) }