Skip to content

Commit 25d5dab

Browse files
author
molty3000
committed
feat: extensive test coverage (86.8%) + version from VCS
Coverage by package: - kode: 100% — Config, New, Run, Close, toolAdapter - internal/tool: 100% — Registry CRUD, duplicates - internal/loop: 100% — ReAct engine, buildToolDefs all paths - internal/llm: 91.2% — CallParams, thinking, Call() via httptest - cmd/kode: 77.1% — parseRunFlags, shell tool, sandbox setup Total: 86.8% (remaining: os.Exit, Docker-in-Docker, error paths for json.Marshal/io.ReadAll) New tests added: - shell_test.go: 16 tests for shell tool (local + docker exec integration) - main_test.go: 22 tests for flag parsing, version, sandbox commands - kode_test.go: Agent.Run, Close (with/without sandbox), toolAdapter - client_test.go: New(), Call() with httptest, 401, invalid JSON - loop_test.go: buildToolDefs string schema paths Version now reads from debug.ReadBuildInfo (VCS tag > revision > dev) with ldflags override.
1 parent db38bcf commit 25d5dab

7 files changed

Lines changed: 1340 additions & 84 deletions

File tree

cmd/kode/main.go

Lines changed: 147 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import (
66
"os"
77
"os/exec"
88
"os/signal"
9+
"runtime/debug"
910
"strings"
1011

1112
"github.com/BackendStack21/kode"
1213
)
1314

15+
// version is set at build time via ldflags: -ldflags "-X main.version=v0.2.1"
16+
// Falls back to VCS tag from debug.ReadBuildInfo, then to "dev".
17+
var version string
18+
1419
const defaultSystem = `You are kode, an autonomous AI coding agent. You solve tasks by reasoning step by step, then executing tools.
1520
1621
Rules:
@@ -27,148 +32,212 @@ func main() {
2732

2833
switch os.Args[1] {
2934
case "run":
30-
runCmd()
35+
if err := run(os.Args[2:]); err != nil {
36+
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
37+
os.Exit(1)
38+
}
3139
case "version":
32-
fmt.Println("kode v0.2.0")
40+
fmt.Println("kode", getVersion())
3341
default:
3442
fmt.Fprintf(os.Stderr, "kode: unknown command %q\n", os.Args[1])
3543
printUsage()
3644
os.Exit(1)
3745
}
3846
}
3947

40-
func printUsage() {
41-
fmt.Println(`Usage:
42-
kode run [flags] <task>
43-
kode version
44-
45-
Flags:
46-
--model <name> LLM model (default: deepseek-chat)
47-
--base-url <url> API endpoint (default: https://api.deepseek.com/v1)
48-
--max-iter <n> Max think->act cycles (default: 90)
49-
--thinking <level> Reasoning depth: enabled|disabled (Deepseek) or low|medium|high (OpenAI o-series)
50-
--sandbox Run in isolated Docker container
51-
--system <prompt> System prompt override`)
48+
// runFlags holds the parsed CLI flags for `kode run`.
49+
type runFlags struct {
50+
Model string
51+
BaseURL string
52+
System string
53+
Thinking string
54+
MaxIter int
55+
Sandbox bool
56+
Task string
5257
}
5358

54-
func runCmd() {
55-
args := os.Args[2:]
56-
var model, baseURL, system, thinking string
57-
maxIter := 90
58-
sandbox := false
59+
// parseRunFlags parses `kode run` arguments and returns the parsed flags.
60+
// Exported for testing.
61+
func parseRunFlags(args []string) (runFlags, error) {
62+
var f runFlags
63+
f.MaxIter = 90
5964

6065
i := 0
6166
for i < len(args)-1 {
6267
switch args[i] {
6368
case "--model":
64-
model = args[i+1]
69+
f.Model = args[i+1]
6570
i += 2
6671
case "--base-url":
67-
baseURL = args[i+1]
72+
f.BaseURL = args[i+1]
6873
i += 2
6974
case "--max-iter":
70-
fmt.Sscanf(args[i+1], "%d", &maxIter)
75+
fmt.Sscanf(args[i+1], "%d", &f.MaxIter)
7176
i += 2
7277
case "--system":
73-
system = args[i+1]
78+
f.System = args[i+1]
7479
i += 2
7580
case "--thinking":
76-
thinking = args[i+1]
81+
f.Thinking = args[i+1]
7782
i += 2
7883
case "--sandbox":
79-
sandbox = true
84+
f.Sandbox = true
8085
i++
8186
default:
8287
// Not a flag — treat remaining as the task
8388
goto done
8489
}
8590
}
8691
done:
87-
task := strings.Join(args[i:], " ")
88-
if task == "" {
89-
fmt.Fprintln(os.Stderr, "kode: no task provided")
90-
os.Exit(1)
92+
f.Task = strings.Join(args[i:], " ")
93+
if f.Task == "" {
94+
return f, fmt.Errorf("no task provided")
9195
}
96+
return f, nil
97+
}
9298

93-
if system == "" {
94-
system = defaultSystem
99+
func printUsage() {
100+
fmt.Println(`Usage:
101+
kode run [flags] <task>
102+
kode version
103+
104+
Flags:
105+
--model <name> LLM model (default: deepseek-chat)
106+
--base-url <url> API endpoint (default: https://api.deepseek.com/v1)
107+
--max-iter <n> Max think->act cycles (default: 90)
108+
--thinking <level> Reasoning depth: enabled|disabled (Deepseek) or low|medium|high (OpenAI o-series)
109+
--sandbox Run in isolated Docker container
110+
--system <prompt> System prompt override`)
111+
}
112+
113+
// run executes the `kode run` command and returns an error on failure.
114+
// The caller is responsible for printing the error and calling os.Exit.
115+
func run(args []string) error {
116+
f, err := parseRunFlags(args)
117+
if err != nil {
118+
return err
95119
}
96120

97-
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
98-
defer cancel()
121+
if f.System == "" {
122+
f.System = defaultSystem
123+
}
99124

100-
// --- sandbox setup ---
125+
// Sandbox setup
101126
var sandboxCleanup func() error
102127
tools := builtinTools()
103128

104-
if sandbox {
105-
containerName := fmt.Sprintf("kode-%d", os.Getpid())
106-
fmt.Fprintf(os.Stderr, "kode: starting sandbox container %s...\n", containerName)
107-
108-
wd, err := os.Getwd()
129+
if f.Sandbox {
130+
cleanup, err := setupSandbox(tools)
109131
if err != nil {
110-
fmt.Fprintf(os.Stderr, "kode: sandbox: getwd: %v\n", err)
111-
os.Exit(1)
112-
}
113-
114-
createCmd := exec.Command("docker", "run",
115-
"--rm", // destroy on exit
116-
"--detach", // run in background
117-
"--name", containerName,
118-
"--cap-drop", "ALL", // no capabilities
119-
"--security-opt", "no-new-privileges", // no privilege escalation
120-
"--network", "none", // no network
121-
"--tmpfs", "/tmp:noexec", // no executable temp files
122-
"-v", wd+":/workspace", // working dir (read-write inside sandbox)
123-
"alpine:latest",
124-
"sleep", "infinity",
125-
)
126-
createCmd.Stderr = os.Stderr
127-
if err := createCmd.Run(); err != nil {
128-
fmt.Fprintf(os.Stderr, "kode: sandbox: failed to create container: %v\n", err)
129-
os.Exit(1)
130-
}
131-
132-
sandboxCleanup = func() error {
133-
fmt.Fprintf(os.Stderr, "kode: destroying sandbox container %s...\n", containerName)
134-
return exec.Command("docker", "rm", "-f", containerName).Run()
132+
return fmt.Errorf("sandbox: %w", err)
135133
}
136-
137-
// Wire the shell tool to execute commands inside the sandbox.
138-
tools[0].(*shellTool).containerName = containerName
134+
sandboxCleanup = cleanup
139135
}
140136

141137
agent, err := kode.New(kode.Config{
142-
Model: model,
143-
BaseURL: baseURL,
144-
MaxIterations: maxIter,
145-
SystemMessage: system,
146-
Thinking: thinking,
138+
Model: f.Model,
139+
BaseURL: f.BaseURL,
140+
MaxIterations: f.MaxIter,
141+
SystemMessage: f.System,
142+
Thinking: f.Thinking,
147143
Tools: tools,
148144
SandboxCleanup: sandboxCleanup,
149145
})
150146
if err != nil {
151-
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
152-
os.Exit(1)
147+
return err
153148
}
154149
defer agent.Close()
155150

156-
modelName := model
151+
modelName := f.Model
157152
if modelName == "" {
158153
modelName = "deepseek-chat"
159154
}
160155
fmt.Fprintf(os.Stderr, "kode: %s thinking...\n", modelName)
161156

162-
result, err := agent.Run(ctx, task)
157+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
158+
defer cancel()
159+
160+
result, err := agent.Run(ctx, f.Task)
163161
if err != nil {
164-
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
165-
os.Exit(1)
162+
return err
166163
}
167164
fmt.Println(result)
165+
return nil
166+
}
167+
168+
// setupSandbox creates a Docker container and wires the shell tool to use it.
169+
// Returns a cleanup function that destroys the container.
170+
func setupSandbox(tools []kode.Tool) (func() error, error) {
171+
containerName := fmt.Sprintf("kode-%d", os.Getpid())
172+
fmt.Fprintf(os.Stderr, "kode: starting sandbox container %s...\n", containerName)
173+
174+
wd, err := os.Getwd()
175+
if err != nil {
176+
return nil, fmt.Errorf("getwd: %w", err)
177+
}
178+
179+
createCmd := exec.Command("docker", "run",
180+
"--rm", // destroy on exit
181+
"--detach", // run in background
182+
"--name", containerName,
183+
"--cap-drop", "ALL", // no capabilities
184+
"--security-opt", "no-new-privileges", // no privilege escalation
185+
"--network", "none", // no network
186+
"--tmpfs", "/tmp:noexec", // no executable temp files
187+
"-v", wd+":/workspace", // working dir (read-write inside sandbox)
188+
"alpine:latest",
189+
"sleep", "infinity",
190+
)
191+
createCmd.Stderr = os.Stderr
192+
if err := createCmd.Run(); err != nil {
193+
return nil, fmt.Errorf("failed to create container: %w", err)
194+
}
195+
196+
cleanup := func() error {
197+
fmt.Fprintf(os.Stderr, "kode: destroying sandbox container %s...\n", containerName)
198+
return exec.Command("docker", "rm", "-f", containerName).Run()
199+
}
200+
201+
// Wire the shell tool to execute commands inside the sandbox.
202+
tools[0].(*shellTool).containerName = containerName
203+
return cleanup, nil
168204
}
169205

170206
func builtinTools() []kode.Tool {
171207
return []kode.Tool{
172208
&shellTool{},
173209
}
210+
}
211+
212+
// getVersion returns the version string. Resolution order:
213+
// 1. ldflags override (-X main.version=v0.2.1)
214+
// 2. VCS tag from debug.ReadBuildInfo (when built with go install)
215+
// 3. VCS revision (short commit hash)
216+
// 4. "dev" (local go build without VCS info)
217+
func getVersion() string {
218+
if version != "" {
219+
return version
220+
}
221+
info, ok := debug.ReadBuildInfo()
222+
if !ok {
223+
return "dev"
224+
}
225+
var revision string
226+
for _, s := range info.Settings {
227+
switch s.Key {
228+
case "vcs.revision":
229+
revision = s.Value
230+
if len(revision) > 7 {
231+
revision = revision[:7]
232+
}
233+
case "vcs.tag":
234+
if s.Value != "" {
235+
return s.Value
236+
}
237+
}
238+
}
239+
if revision != "" {
240+
return revision
241+
}
242+
return "dev"
174243
}

0 commit comments

Comments
 (0)