code.oscarkilo.com/okg

Hash:
98f1be1e667523f7028e04e93342148134a73bac
Author:
Igor Naverniouk <[email protected]>
Date:
Fri May 29 21:03:29 2026 -0400
Message:
Default okg to the production klee host Codex (and any other agent or human inside a klee clone) shouldn't have to set OKG_HOST=https://code.oscarkilo.com on every invocation just to talk to the one production host that exists. localhost was a dev-friendly default but the wrong production default. - config.go: hoist the host fallback into a DefaultHost const set to https://code.oscarkilo.com. The config-file and OKG_HOST env-var paths still win, so dev/local work is unaffected by --host or OKG_HOST. - auth.go: use DefaultHost for both the interactive prompt text and the empty-input fallback (was hard-coded to localhost in both spots, contradicting the new default). - okg_test.go: add TestMain that pins OKG_HOST=http://localhost:42069 for the whole test binary, so tests can't accidentally reach the prod host. TestConfigDefaultHost is updated to explicitly bypass the guardrail (t.Setenv to empty + HOME=TempDir) and assert the new prod default. - README.md: state the prod default in Setup; drop the now-redundant OKG_HOST= line and --host flag from the env-var / login examples; add a one-liner on how to override the host for dev/local work.
diff --git a/README.md b/README.md
index 09bef45..7b3ac78 100644
--- a/README.md
+++ b/README.md
@@ -21,18 +21,21 @@ cd okg && go build .

## Setup

+By default okg talks to the production klee host
+(`https://code.oscarkilo.com`); the only setup you need is to give it
+your API key. Either:
+
```bash
-# Interactive login (saves to ~/.config/okg/config.json)
+# Interactive login — saves host + key to ~/.config/okg/config.json.
okg auth login

-# With flags
-okg auth login --host https://code.oscarkilo.com --user igor
-
-# Or use environment variables
-export OKG_HOST=https://code.oscarkilo.com
+# Or non-interactive: just set the API key.
export KLEX_API_KEY=your-api-key
```

+Override the host for dev/local work with `--host` on `auth login`,
+or with the `OKG_HOST` env var.
+
## Commands

```
diff --git a/auth.go b/auth.go
index 589977e..4a8880b 100644
--- a/auth.go
+++ b/auth.go
@@ -46,12 +46,11 @@ func runAuthLogin(args []string) error {
reader := bufio.NewReader(os.Stdin)

if host == "" {
- fmt.Print(
- "Host (default http://localhost:42069): ")
+ fmt.Printf("Host (default %s): ", DefaultHost)
line, _ := reader.ReadString('\n')
host = strings.TrimSpace(line)
if host == "" {
- host = "http://localhost:42069"
+ host = DefaultHost
}
}

diff --git a/config.go b/config.go
index 2ba12f5..8285075 100644
--- a/config.go
+++ b/config.go
@@ -5,6 +5,11 @@ import "fmt"
import "os"
import "path/filepath"

+// DefaultHost is the production klee host. There is exactly one for
+// the foreseeable future. It's used by loadConfig (as the host
+// fallback) and by `okg auth login` (as the interactive default).
+const DefaultHost = "https://code.oscarkilo.com"
+
type Config struct {
Host string `json:"host"`
ApiKey string `json:"api_key"`
@@ -38,9 +43,10 @@ func loadConfig() (*Config, error) {
c.ApiKey = v
}

- // Defaults.
+ // Default to prod. Tests set OKG_HOST in TestMain to a localhost
+ // URL so they can't accidentally reach it.
if c.Host == "" {
- c.Host = "http://localhost:42069"
+ c.Host = DefaultHost
}

return c, nil
diff --git a/okg_test.go b/okg_test.go
index 8227575..e03fb07 100644
--- a/okg_test.go
+++ b/okg_test.go
@@ -10,6 +10,15 @@ import "time"

import "oscarkilo.com/okg/klee"

+// TestMain prevents the entire test binary from accidentally reaching
+// the production klee host (which is the default in loadConfig). Set
+// it once here; individual tests that need a specific URL (e.g. a
+// mock httptest server) still set OKG_HOST themselves.
+func TestMain(m *testing.M) {
+ os.Setenv("OKG_HOST", "http://localhost:42069")
+ os.Exit(m.Run())
+}
+
func TestRepoRegex(t *testing.T) {
check := func(url, want string) {
t.Helper()
@@ -153,15 +162,18 @@ func TestConfigEnvOverrides(t *testing.T) {
}

func TestConfigDefaultHost(t *testing.T) {
- os.Unsetenv("OKG_HOST")
- os.Unsetenv("KLEX_API_KEY")
+ // Bypass the TestMain guardrail and isolate from any real
+ // ~/.config/okg/config.json so we can verify the prod default.
+ t.Setenv("HOME", t.TempDir())
+ t.Setenv("OKG_HOST", "")
+ t.Setenv("KLEX_API_KEY", "")
cfg, err := loadConfig()
if err != nil {
t.Fatal(err)
}
- if cfg.Host != "http://localhost:42069" {
+ if cfg.Host != "https://code.oscarkilo.com" {
t.Errorf(
- "Host: want http://localhost:42069, got %q",
+ "Host: want https://code.oscarkilo.com, got %q",
cfg.Host)
}
}
a/README.md
b/README.md
1
# okg — Oscar Kilo Git CLI
1
# okg — Oscar Kilo Git CLI
2
2
3
A `gh`-style command-line tool for interacting with
3
A `gh`-style command-line tool for interacting with
4
[klee](https://code.oscarkilo.com), the Oscar Kilo git server.
4
[klee](https://code.oscarkilo.com), the Oscar Kilo git server.
5
5
6
Designed for both humans and AI agents (Claude, OpenClaw) to
6
Designed for both humans and AI agents (Claude, OpenClaw) to
7
use directly from the command line.
7
use directly from the command line.
8
8
9
## Install
9
## Install
10
10
11
```bash
11
```bash
12
go install oscarkilo.com/okg@latest
12
go install oscarkilo.com/okg@latest
13
```
13
```
14
14
15
Or build from source:
15
Or build from source:
16
16
17
```bash
17
```bash
18
git clone https://code.oscarkilo.com/okg
18
git clone https://code.oscarkilo.com/okg
19
cd okg && go build .
19
cd okg && go build .
20
```
20
```
21
21
22
## Setup
22
## Setup
23
23
24
By default okg talks to the production klee host
25
(`https://code.oscarkilo.com`); the only setup you need is to give it
26
your API key. Either:
27
24
```bash
28
```bash
25
# Interactive login (saves to ~/.config/okg/config.json)
29
# Interactive login saves host + key to ~/.config/okg/config.json.
26
okg auth login
30
okg auth login
27
31
28
# With flags
32
# Or non-interactive: just set the API key.
29
okg auth login --host https://code.oscarkilo.com --user igor
30
31
# Or use environment variables
32
export OKG_HOST=https://code.oscarkilo.com
33
export KLEX_API_KEY=your-api-key
33
export KLEX_API_KEY=your-api-key
34
```
34
```
35
35
36
Override the host for dev/local work with `--host` on `auth login`,
37
or with the `OKG_HOST` env var.
38
36
## Commands
39
## Commands
37
40
38
```
41
```
39
okg repo list
42
okg repo list
40
43
41
okg pr list [--state open|closed]
44
okg pr list [--state open|closed]
42
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
45
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
43
okg pr view NUMBER
46
okg pr view NUMBER
44
okg pr diff NUMBER
47
okg pr diff NUMBER
45
okg pr comment NUMBER --body BODY [--approve | --request-changes]
48
okg pr comment NUMBER --body BODY [--approve | --request-changes]
46
okg pr merge NUMBER
49
okg pr merge NUMBER
47
okg pr close NUMBER
50
okg pr close NUMBER
48
okg pr reopen NUMBER
51
okg pr reopen NUMBER
49
52
50
okg auth login [--host HOST] [--user USERNAME]
53
okg auth login [--host HOST] [--user USERNAME]
51
```
54
```
52
55
53
### Flags
56
### Flags
54
57
55
- `--repo REPO` overrides auto-detected repo name
58
- `--repo REPO` overrides auto-detected repo name
56
(normally parsed from `git remote get-url origin`)
59
(normally parsed from `git remote get-url origin`)
57
- `--json` outputs raw JSON for any command
60
- `--json` outputs raw JSON for any command
58
- `OKG_REPO` env var also overrides repo detection
61
- `OKG_REPO` env var also overrides repo detection
59
62
60
## Repo Detection
63
## Repo Detection
61
64
62
Like `gh`, okg detects the repo from the current directory's
65
Like `gh`, okg detects the repo from the current directory's
63
git remote:
66
git remote:
64
67
65
```
68
```
66
git remote get-url origin
69
git remote get-url origin
67
→ https://code.oscarkilo.com/widget.git
70
→ https://code.oscarkilo.com/widget.git
68
→ repo = "widget"
71
→ repo = "widget"
69
```
72
```
70
73
71
## Dependencies
74
## Dependencies
72
75
73
None beyond the Go standard library.
76
None beyond the Go standard library.
a/auth.go
b/auth.go
1
package main
1
package main
2
2
3
import "bufio"
3
import "bufio"
4
import "fmt"
4
import "fmt"
5
import "os"
5
import "os"
6
import "strings"
6
import "strings"
7
7
8
func runAuth(args []string) error {
8
func runAuth(args []string) error {
9
if len(args) == 0 {
9
if len(args) == 0 {
10
return fmt.Errorf("usage: okg auth login")
10
return fmt.Errorf("usage: okg auth login")
11
}
11
}
12
switch args[0] {
12
switch args[0] {
13
case "login":
13
case "login":
14
return runAuthLogin(args[1:])
14
return runAuthLogin(args[1:])
15
default:
15
default:
16
return fmt.Errorf(
16
return fmt.Errorf(
17
"unknown auth command: %s", args[0])
17
"unknown auth command: %s", args[0])
18
}
18
}
19
}
19
}
20
20
21
func runAuthLogin(args []string) error {
21
func runAuthLogin(args []string) error {
22
host := ""
22
host := ""
23
user := ""
23
user := ""
24
for i := 0; i < len(args); i++ {
24
for i := 0; i < len(args); i++ {
25
switch args[i] {
25
switch args[i] {
26
case "--host":
26
case "--host":
27
i++
27
i++
28
if i >= len(args) {
28
if i >= len(args) {
29
return fmt.Errorf(
29
return fmt.Errorf(
30
"--host requires a value")
30
"--host requires a value")
31
}
31
}
32
host = args[i]
32
host = args[i]
33
case "--user":
33
case "--user":
34
i++
34
i++
35
if i >= len(args) {
35
if i >= len(args) {
36
return fmt.Errorf(
36
return fmt.Errorf(
37
"--user requires a value")
37
"--user requires a value")
38
}
38
}
39
user = args[i]
39
user = args[i]
40
default:
40
default:
41
return fmt.Errorf(
41
return fmt.Errorf(
42
"unknown flag: %s", args[i])
42
"unknown flag: %s", args[i])
43
}
43
}
44
}
44
}
45
45
46
reader := bufio.NewReader(os.Stdin)
46
reader := bufio.NewReader(os.Stdin)
47
47
48
if host == "" {
48
if host == "" {
49
fmt.Print(
49
fmt.Printf("Host (default %s): ", DefaultHost)
50
"Host (default http://localhost:42069): ")
51
line, _ := reader.ReadString('\n')
50
line, _ := reader.ReadString('\n')
52
host = strings.TrimSpace(line)
51
host = strings.TrimSpace(line)
53
if host == "" {
52
if host == "" {
54
host = "http://localhost:42069"
53
host = DefaultHost
55
}
54
}
56
}
55
}
57
56
58
fmt.Print("API key: ")
57
fmt.Print("API key: ")
59
api_key, _ := reader.ReadString('\n')
58
api_key, _ := reader.ReadString('\n')
60
api_key = strings.TrimSpace(api_key)
59
api_key = strings.TrimSpace(api_key)
61
if api_key == "" {
60
if api_key == "" {
62
return fmt.Errorf("API key is required")
61
return fmt.Errorf("API key is required")
63
}
62
}
64
63
65
cfg := &Config{Host: host, ApiKey: api_key}
64
cfg := &Config{Host: host, ApiKey: api_key}
66
if err := saveConfig(cfg); err != nil {
65
if err := saveConfig(cfg); err != nil {
67
return fmt.Errorf("saving config: %v", err)
66
return fmt.Errorf("saving config: %v", err)
68
}
67
}
69
fmt.Printf("Saved config to %s\n", configPath())
68
fmt.Printf("Saved config to %s\n", configPath())
70
69
71
// If --user given, call profile edit to map
70
// If --user given, call profile edit to map
72
// the API key to this username in mock who.
71
// the API key to this username in mock who.
73
if user != "" {
72
if user != "" {
74
cl := newKleeClient(cfg)
73
cl := newKleeClient(cfg)
75
payload := map[string]string{
74
payload := map[string]string{
76
"username": user,
75
"username": user,
77
"name": user,
76
"name": user,
78
}
77
}
79
err := cl.PostJSON(
78
err := cl.PostJSON(
80
"/login/profile/edit", payload, nil)
79
"/login/profile/edit", payload, nil)
81
if err != nil {
80
if err != nil {
82
return fmt.Errorf(
81
return fmt.Errorf(
83
"setting username: %v", err)
82
"setting username: %v", err)
84
}
83
}
85
fmt.Printf("Authenticated as %s\n", user)
84
fmt.Printf("Authenticated as %s\n", user)
86
}
85
}
87
86
88
return nil
87
return nil
89
}
88
}
a/config.go
b/config.go
1
package main
1
package main
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "fmt"
4
import "fmt"
5
import "os"
5
import "os"
6
import "path/filepath"
6
import "path/filepath"
7
7
8
// DefaultHost is the production klee host. There is exactly one for
9
// the foreseeable future. It's used by loadConfig (as the host
10
// fallback) and by `okg auth login` (as the interactive default).
11
const DefaultHost = "https://code.oscarkilo.com"
12
8
type Config struct {
13
type Config struct {
9
Host string `json:"host"`
14
Host string `json:"host"`
10
ApiKey string `json:"api_key"`
15
ApiKey string `json:"api_key"`
11
}
16
}
12
17
13
func configPath() string {
18
func configPath() string {
14
home, err := os.UserHomeDir()
19
home, err := os.UserHomeDir()
15
if err != nil {
20
if err != nil {
16
return ""
21
return ""
17
}
22
}
18
return filepath.Join(home, ".config", "okg", "config.json")
23
return filepath.Join(home, ".config", "okg", "config.json")
19
}
24
}
20
25
21
func loadConfig() (*Config, error) {
26
func loadConfig() (*Config, error) {
22
c := &Config{}
27
c := &Config{}
23
28
24
// Load from file.
29
// Load from file.
25
path := configPath()
30
path := configPath()
26
if path != "" {
31
if path != "" {
27
data, err := os.ReadFile(path)
32
data, err := os.ReadFile(path)
28
if err == nil {
33
if err == nil {
29
json.Unmarshal(data, c)
34
json.Unmarshal(data, c)
30
}
35
}
31
}
36
}
32
37
33
// Env overrides.
38
// Env overrides.
34
if v := os.Getenv("OKG_HOST"); v != "" {
39
if v := os.Getenv("OKG_HOST"); v != "" {
35
c.Host = v
40
c.Host = v
36
}
41
}
37
if v := os.Getenv("KLEX_API_KEY"); v != "" {
42
if v := os.Getenv("KLEX_API_KEY"); v != "" {
38
c.ApiKey = v
43
c.ApiKey = v
39
}
44
}
40
45
41
// Defaults.
46
// Default to prod. Tests set OKG_HOST in TestMain to a localhost
47
// URL so they can't accidentally reach it.
42
if c.Host == "" {
48
if c.Host == "" {
43
c.Host = "http://localhost:42069"
49
c.Host = DefaultHost
44
}
50
}
45
51
46
return c, nil
52
return c, nil
47
}
53
}
48
54
49
func saveConfig(c *Config) error {
55
func saveConfig(c *Config) error {
50
path := configPath()
56
path := configPath()
51
if path == "" {
57
if path == "" {
52
return fmt.Errorf("cannot determine home directory")
58
return fmt.Errorf("cannot determine home directory")
53
}
59
}
54
dir := filepath.Dir(path)
60
dir := filepath.Dir(path)
55
if err := os.MkdirAll(dir, 0700); err != nil {
61
if err := os.MkdirAll(dir, 0700); err != nil {
56
return fmt.Errorf("mkdir %s: %v", dir, err)
62
return fmt.Errorf("mkdir %s: %v", dir, err)
57
}
63
}
58
data, err := json.MarshalIndent(c, "", " ")
64
data, err := json.MarshalIndent(c, "", " ")
59
if err != nil {
65
if err != nil {
60
return err
66
return err
61
}
67
}
62
return os.WriteFile(path, data, 0600)
68
return os.WriteFile(path, data, 0600)
63
}
69
}
a/okg_test.go
b/okg_test.go
1
package main
1
package main
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "net/http"
4
import "net/http"
5
import "net/http/httptest"
5
import "net/http/httptest"
6
import "os"
6
import "os"
7
import "strings"
7
import "strings"
8
import "testing"
8
import "testing"
9
import "time"
9
import "time"
10
10
11
import "oscarkilo.com/okg/klee"
11
import "oscarkilo.com/okg/klee"
12
12
13
// TestMain prevents the entire test binary from accidentally reaching
14
// the production klee host (which is the default in loadConfig). Set
15
// it once here; individual tests that need a specific URL (e.g. a
16
// mock httptest server) still set OKG_HOST themselves.
17
func TestMain(m *testing.M) {
18
os.Setenv("OKG_HOST", "http://localhost:42069")
19
os.Exit(m.Run())
20
}
21
13
func TestRepoRegex(t *testing.T) {
22
func TestRepoRegex(t *testing.T) {
14
check := func(url, want string) {
23
check := func(url, want string) {
15
t.Helper()
24
t.Helper()
16
m := repoRegex.FindStringSubmatch(url)
25
m := repoRegex.FindStringSubmatch(url)
17
if want == "" {
26
if want == "" {
18
if m != nil {
27
if m != nil {
19
t.Errorf(
28
t.Errorf(
20
"%q: want no match, got %q", url, m[1])
29
"%q: want no match, got %q", url, m[1])
21
}
30
}
22
return
31
return
23
}
32
}
24
if m == nil {
33
if m == nil {
25
t.Errorf(
34
t.Errorf(
26
"%q: want %q, got no match", url, want)
35
"%q: want %q, got no match", url, want)
27
return
36
return
28
}
37
}
29
if m[1] != want {
38
if m[1] != want {
30
t.Errorf(
39
t.Errorf(
31
"%q: want %q, got %q", url, want, m[1])
40
"%q: want %q, got %q", url, want, m[1])
32
}
41
}
33
}
42
}
34
check(
43
check(
35
"https://code.oscarkilo.com/widget.git",
44
"https://code.oscarkilo.com/widget.git",
36
"widget")
45
"widget")
37
check(
46
check(
38
"https://code.oscarkilo.com/klee.git",
47
"https://code.oscarkilo.com/klee.git",
39
"klee")
48
"klee")
40
check(
49
check(
41
"https://code.oscarkilo.com/my-repo.git",
50
"https://code.oscarkilo.com/my-repo.git",
42
"my-repo")
51
"my-repo")
43
check(
52
check(
44
"https://code.oscarkilo.com/a123.git",
53
"https://code.oscarkilo.com/a123.git",
45
"a123")
54
"a123")
46
check(
55
check(
47
"https://github.com/foo/bar.git",
56
"https://github.com/foo/bar.git",
48
"")
57
"")
49
check("not-a-url", "")
58
check("not-a-url", "")
50
}
59
}
51
60
52
func TestResolveRepo(t *testing.T) {
61
func TestResolveRepo(t *testing.T) {
53
// Flag takes priority.
62
// Flag takes priority.
54
repo, err := resolveRepo("from-flag")
63
repo, err := resolveRepo("from-flag")
55
if err != nil {
64
if err != nil {
56
t.Fatal(err)
65
t.Fatal(err)
57
}
66
}
58
if repo != "from-flag" {
67
if repo != "from-flag" {
59
t.Errorf("want from-flag, got %q", repo)
68
t.Errorf("want from-flag, got %q", repo)
60
}
69
}
61
70
62
// Env var takes priority over detection.
71
// Env var takes priority over detection.
63
os.Setenv("OKG_REPO", "from-env")
72
os.Setenv("OKG_REPO", "from-env")
64
defer os.Unsetenv("OKG_REPO")
73
defer os.Unsetenv("OKG_REPO")
65
repo, err = resolveRepo("")
74
repo, err = resolveRepo("")
66
if err != nil {
75
if err != nil {
67
t.Fatal(err)
76
t.Fatal(err)
68
}
77
}
69
if repo != "from-env" {
78
if repo != "from-env" {
70
t.Errorf("want from-env, got %q", repo)
79
t.Errorf("want from-env, got %q", repo)
71
}
80
}
72
}
81
}
73
82
74
func TestParsePRFlags(t *testing.T) {
83
func TestParsePRFlags(t *testing.T) {
75
f, rest, err := parsePRFlags([]string{
84
f, rest, err := parsePRFlags([]string{
76
"--repo", "widget", "--json", "42",
85
"--repo", "widget", "--json", "42",
77
})
86
})
78
if err != nil {
87
if err != nil {
79
t.Fatal(err)
88
t.Fatal(err)
80
}
89
}
81
if f.repo != "widget" {
90
if f.repo != "widget" {
82
t.Errorf(
91
t.Errorf(
83
"repo: want widget, got %q", f.repo)
92
"repo: want widget, got %q", f.repo)
84
}
93
}
85
if !f.asJSON {
94
if !f.asJSON {
86
t.Error("asJSON: want true")
95
t.Error("asJSON: want true")
87
}
96
}
88
if len(rest) != 1 || rest[0] != "42" {
97
if len(rest) != 1 || rest[0] != "42" {
89
t.Errorf("rest: want [42], got %v", rest)
98
t.Errorf("rest: want [42], got %v", rest)
90
}
99
}
91
}
100
}
92
101
93
func TestParsePRFlagsEmpty(t *testing.T) {
102
func TestParsePRFlagsEmpty(t *testing.T) {
94
f, rest, err := parsePRFlags(nil)
103
f, rest, err := parsePRFlags(nil)
95
if err != nil {
104
if err != nil {
96
t.Fatal(err)
105
t.Fatal(err)
97
}
106
}
98
if f.repo != "" {
107
if f.repo != "" {
99
t.Errorf(
108
t.Errorf(
100
"repo: want empty, got %q", f.repo)
109
"repo: want empty, got %q", f.repo)
101
}
110
}
102
if f.asJSON {
111
if f.asJSON {
103
t.Error("asJSON: want false")
112
t.Error("asJSON: want false")
104
}
113
}
105
if len(rest) != 0 {
114
if len(rest) != 0 {
106
t.Errorf("rest: want empty, got %v", rest)
115
t.Errorf("rest: want empty, got %v", rest)
107
}
116
}
108
}
117
}
109
118
110
func TestParsePRFlagsMissingValue(t *testing.T) {
119
func TestParsePRFlagsMissingValue(t *testing.T) {
111
_, _, err := parsePRFlags([]string{"--repo"})
120
_, _, err := parsePRFlags([]string{"--repo"})
112
if err == nil {
121
if err == nil {
113
t.Error("want error for --repo without value")
122
t.Error("want error for --repo without value")
114
}
123
}
115
}
124
}
116
125
117
func TestAge(t *testing.T) {
126
func TestAge(t *testing.T) {
118
check := func(d time.Duration, want string) {
127
check := func(d time.Duration, want string) {
119
t.Helper()
128
t.Helper()
120
got := age(time.Now().Add(-d))
129
got := age(time.Now().Add(-d))
121
if got != want {
130
if got != want {
122
t.Errorf(
131
t.Errorf(
123
"age(-%v): want %q, got %q",
132
"age(-%v): want %q, got %q",
124
d, want, got)
133
d, want, got)
125
}
134
}
126
}
135
}
127
check(30*time.Second, "just now")
136
check(30*time.Second, "just now")
128
check(5*time.Minute, "5m")
137
check(5*time.Minute, "5m")
129
check(3*time.Hour, "3h")
138
check(3*time.Hour, "3h")
130
check(48*time.Hour, "2d")
139
check(48*time.Hour, "2d")
131
}
140
}
132
141
133
func TestConfigEnvOverrides(t *testing.T) {
142
func TestConfigEnvOverrides(t *testing.T) {
134
os.Setenv("OKG_HOST", "http://test:1234")
143
os.Setenv("OKG_HOST", "http://test:1234")
135
os.Setenv("KLEX_API_KEY", "env-key")
144
os.Setenv("KLEX_API_KEY", "env-key")
136
defer os.Unsetenv("OKG_HOST")
145
defer os.Unsetenv("OKG_HOST")
137
defer os.Unsetenv("KLEX_API_KEY")
146
defer os.Unsetenv("KLEX_API_KEY")
138
147
139
cfg, err := loadConfig()
148
cfg, err := loadConfig()
140
if err != nil {
149
if err != nil {
141
t.Fatal(err)
150
t.Fatal(err)
142
}
151
}
143
if cfg.Host != "http://test:1234" {
152
if cfg.Host != "http://test:1234" {
144
t.Errorf(
153
t.Errorf(
145
"Host: want http://test:1234, got %q",
154
"Host: want http://test:1234, got %q",
146
cfg.Host)
155
cfg.Host)
147
}
156
}
148
if cfg.ApiKey != "env-key" {
157
if cfg.ApiKey != "env-key" {
149
t.Errorf(
158
t.Errorf(
150
"ApiKey: want env-key, got %q",
159
"ApiKey: want env-key, got %q",
151
cfg.ApiKey)
160
cfg.ApiKey)
152
}
161
}
153
}
162
}
154
163
155
func TestConfigDefaultHost(t *testing.T) {
164
func TestConfigDefaultHost(t *testing.T) {
156
os.Unsetenv("OKG_HOST")
165
// Bypass the TestMain guardrail and isolate from any real
157
os.Unsetenv("KLEX_API_KEY")
166
// ~/.config/okg/config.json so we can verify the prod default.
167
t.Setenv("HOME", t.TempDir())
168
t.Setenv("OKG_HOST", "")
169
t.Setenv("KLEX_API_KEY", "")
158
cfg, err := loadConfig()
170
cfg, err := loadConfig()
159
if err != nil {
171
if err != nil {
160
t.Fatal(err)
172
t.Fatal(err)
161
}
173
}
162
if cfg.Host != "http://localhost:42069" {
174
if cfg.Host != "https://code.oscarkilo.com" {
163
t.Errorf(
175
t.Errorf(
164
"Host: want http://localhost:42069, got %q",
176
"Host: want https://code.oscarkilo.com, got %q",
165
cfg.Host)
177
cfg.Host)
166
}
178
}
167
}
179
}
168
180
169
func TestRunRepoCreateArgs(t *testing.T) {
181
func TestRunRepoCreateArgs(t *testing.T) {
170
mock := newMockKleeRepo(t, 204, "")
182
mock := newMockKleeRepo(t, 204, "")
171
defer mock.Close()
183
defer mock.Close()
172
184
173
os.Setenv("OKG_HOST", mock.URL)
185
os.Setenv("OKG_HOST", mock.URL)
174
os.Setenv("KLEX_API_KEY", "test-key")
186
os.Setenv("KLEX_API_KEY", "test-key")
175
defer os.Unsetenv("OKG_HOST")
187
defer os.Unsetenv("OKG_HOST")
176
defer os.Unsetenv("KLEX_API_KEY")
188
defer os.Unsetenv("KLEX_API_KEY")
177
189
178
err := runRepoCreate([]string{
190
err := runRepoCreate([]string{
179
"my-repo", "--reader", "igor.agents",
191
"my-repo", "--reader", "igor.agents",
180
})
192
})
181
if err != nil {
193
if err != nil {
182
t.Fatal(err)
194
t.Fatal(err)
183
}
195
}
184
if mock.req.RepoName != "my-repo" {
196
if mock.req.RepoName != "my-repo" {
185
t.Errorf(
197
t.Errorf(
186
"repo_name: want my-repo, got %q",
198
"repo_name: want my-repo, got %q",
187
mock.req.RepoName)
199
mock.req.RepoName)
188
}
200
}
189
if mock.req.ReaderUsername != "igor.agents" {
201
if mock.req.ReaderUsername != "igor.agents" {
190
t.Errorf(
202
t.Errorf(
191
"reader: want igor.agents, got %q",
203
"reader: want igor.agents, got %q",
192
mock.req.ReaderUsername)
204
mock.req.ReaderUsername)
193
}
205
}
194
}
206
}
195
207
196
func TestRunRepoCreateNoReader(t *testing.T) {
208
func TestRunRepoCreateNoReader(t *testing.T) {
197
mock := newMockKleeRepo(t, 204, "")
209
mock := newMockKleeRepo(t, 204, "")
198
defer mock.Close()
210
defer mock.Close()
199
211
200
os.Setenv("OKG_HOST", mock.URL)
212
os.Setenv("OKG_HOST", mock.URL)
201
os.Setenv("KLEX_API_KEY", "test-key")
213
os.Setenv("KLEX_API_KEY", "test-key")
202
defer os.Unsetenv("OKG_HOST")
214
defer os.Unsetenv("OKG_HOST")
203
defer os.Unsetenv("KLEX_API_KEY")
215
defer os.Unsetenv("KLEX_API_KEY")
204
216
205
err := runRepoCreate([]string{"my-repo"})
217
err := runRepoCreate([]string{"my-repo"})
206
if err != nil {
218
if err != nil {
207
t.Fatal(err)
219
t.Fatal(err)
208
}
220
}
209
if mock.req.ReaderUsername != "" {
221
if mock.req.ReaderUsername != "" {
210
t.Errorf(
222
t.Errorf(
211
"reader: want empty, got %q",
223
"reader: want empty, got %q",
212
mock.req.ReaderUsername)
224
mock.req.ReaderUsername)
213
}
225
}
214
}
226
}
215
227
216
func TestRunRepoCreateMissingName(t *testing.T) {
228
func TestRunRepoCreateMissingName(t *testing.T) {
217
err := runRepoCreate(nil)
229
err := runRepoCreate(nil)
218
if err == nil {
230
if err == nil {
219
t.Fatal("want error for missing name")
231
t.Fatal("want error for missing name")
220
}
232
}
221
}
233
}
222
234
223
func TestRunRepoCreateReaderMissingValue(
235
func TestRunRepoCreateReaderMissingValue(
224
t *testing.T,
236
t *testing.T,
225
) {
237
) {
226
err := runRepoCreate([]string{
238
err := runRepoCreate([]string{
227
"my-repo", "--reader",
239
"my-repo", "--reader",
228
})
240
})
229
if err == nil {
241
if err == nil {
230
t.Fatal(
242
t.Fatal(
231
"want error for --reader without value")
243
"want error for --reader without value")
232
}
244
}
233
}
245
}
234
246
235
func TestRunRepoCreateUnknownFlag(t *testing.T) {
247
func TestRunRepoCreateUnknownFlag(t *testing.T) {
236
err := runRepoCreate([]string{
248
err := runRepoCreate([]string{
237
"my-repo", "--bogus",
249
"my-repo", "--bogus",
238
})
250
})
239
if err == nil {
251
if err == nil {
240
t.Fatal("want error for unknown flag")
252
t.Fatal("want error for unknown flag")
241
}
253
}
242
}
254
}
243
255
244
func TestRunRepoCreateDuplicateName(
256
func TestRunRepoCreateDuplicateName(
245
t *testing.T,
257
t *testing.T,
246
) {
258
) {
247
err := runRepoCreate([]string{
259
err := runRepoCreate([]string{
248
"my-repo", "extra",
260
"my-repo", "extra",
249
})
261
})
250
if err == nil {
262
if err == nil {
251
t.Fatal(
263
t.Fatal(
252
"want error for extra positional arg")
264
"want error for extra positional arg")
253
}
265
}
254
}
266
}
255
267
256
func TestRunRepoCreateServerError(t *testing.T) {
268
func TestRunRepoCreateServerError(t *testing.T) {
257
mock := newMockKleeRepo(t, 403,
269
mock := newMockKleeRepo(t, 403,
258
"you are not an owner of "+
270
"you are not an owner of "+
259
"klee://code.oscarkilo.com/.new-repo")
271
"klee://code.oscarkilo.com/.new-repo")
260
defer mock.Close()
272
defer mock.Close()
261
273
262
os.Setenv("OKG_HOST", mock.URL)
274
os.Setenv("OKG_HOST", mock.URL)
263
os.Setenv("KLEX_API_KEY", "test-key")
275
os.Setenv("KLEX_API_KEY", "test-key")
264
defer os.Unsetenv("OKG_HOST")
276
defer os.Unsetenv("OKG_HOST")
265
defer os.Unsetenv("KLEX_API_KEY")
277
defer os.Unsetenv("KLEX_API_KEY")
266
278
267
err := runRepoCreate([]string{"my-repo"})
279
err := runRepoCreate([]string{"my-repo"})
268
if err == nil {
280
if err == nil {
269
t.Fatal("want error on 403")
281
t.Fatal("want error on 403")
270
}
282
}
271
if !strings.Contains(err.Error(), "403") {
283
if !strings.Contains(err.Error(), "403") {
272
t.Errorf("want 403 in error, got %q", err)
284
t.Errorf("want 403 in error, got %q", err)
273
}
285
}
274
}
286
}
275
287
276
// mockKleeRepo captures the /.add-repo request.
288
// mockKleeRepo captures the /.add-repo request.
277
type mockKleeRepo struct {
289
type mockKleeRepo struct {
278
*httptest.Server
290
*httptest.Server
279
req klee.CreateRepoRequest
291
req klee.CreateRepoRequest
280
}
292
}
281
293
282
func newMockKleeRepo(
294
func newMockKleeRepo(
283
t *testing.T, status int, body string,
295
t *testing.T, status int, body string,
284
) *mockKleeRepo {
296
) *mockKleeRepo {
285
t.Helper()
297
t.Helper()
286
mk := &mockKleeRepo{}
298
mk := &mockKleeRepo{}
287
mk.Server = httptest.NewServer(
299
mk.Server = httptest.NewServer(
288
http.HandlerFunc(func(
300
http.HandlerFunc(func(
289
w http.ResponseWriter, r *http.Request,
301
w http.ResponseWriter, r *http.Request,
290
) {
302
) {
291
json.NewDecoder(r.Body).Decode(&mk.req)
303
json.NewDecoder(r.Body).Decode(&mk.req)
292
w.WriteHeader(status)
304
w.WriteHeader(status)
293
if body != "" {
305
if body != "" {
294
w.Write([]byte(body))
306
w.Write([]byte(body))
295
}
307
}
296
}))
308
}))
297
return mk
309
return mk
298
}
310
}