Skip to content

Commit 5b763af

Browse files
heusalagroupbotaibuddy
andauthored
cmd/agentcli: port core CLI tests from develop; fix duration precedence (#82)
Ports focused develop tests (help/flags/docs-sync/gpt5 smoke) to validate CLI behavior on main. Align ResolveDuration to treat flag-set zero as explicit, matching develop tests. Refs: #1 Co-authored-by: aibuddy <aibuddy@dev.hg.fi>
1 parent 42f0bfc commit 5b763af

6 files changed

Lines changed: 935 additions & 4 deletions

File tree

cmd/agentcli/cli_docs_sync_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// TestCLIReference_IncludesAllFlagsFromHelp ensures that docs/reference/cli-reference.md
12+
// includes every flag token that appears in the CLI's built-in help output.
13+
func TestCLIReference_IncludesAllFlagsFromHelp(t *testing.T) {
14+
// Render help text via the same function used by the CLI for --help/-h/help.
15+
var b strings.Builder
16+
printUsage(&b)
17+
help := b.String()
18+
19+
// Extract flag tokens from help output. Lines start with two spaces, a hyphen, then the flag.
20+
var flags []string
21+
for _, line := range strings.Split(help, "\n") {
22+
line = strings.TrimRight(line, "\r")
23+
if strings.HasPrefix(line, " -") || strings.HasPrefix(line, " --") {
24+
// Take the first whitespace-separated token as the flag token (e.g., -prompt, --version)
25+
fields := strings.Fields(line)
26+
if len(fields) > 0 {
27+
token := fields[0]
28+
// Normalize trailing punctuation if any
29+
token = strings.TrimRight(token, ":")
30+
flags = append(flags, token)
31+
}
32+
}
33+
}
34+
if len(flags) == 0 {
35+
t.Fatalf("no flags parsed from help; help was:\n%s", help)
36+
}
37+
38+
// Load CLI reference doc. Resolve relative to this test file's directory for robustness.
39+
_, thisFile, _, ok := runtime.Caller(0)
40+
if !ok {
41+
t.Fatalf("runtime.Caller failed")
42+
}
43+
thisDir := filepath.Dir(thisFile)
44+
// Repo root is the parent of the parent (.. of cmd/agentcli => repo root)
45+
repoRoot := filepath.Dir(filepath.Dir(thisDir))
46+
tryPaths := []string{
47+
filepath.Join(repoRoot, "docs", "reference", "cli-reference.md"),
48+
filepath.Join(repoRoot, "README.md"), // fallback so test gives a clearer error if mislocated
49+
}
50+
var data []byte
51+
var err error
52+
var usedPath string
53+
for _, p := range tryPaths {
54+
if b, e := os.ReadFile(p); e == nil {
55+
data, err, usedPath = b, nil, p
56+
break
57+
} else {
58+
err = e
59+
}
60+
}
61+
if data == nil {
62+
t.Fatalf("failed to read CLI reference doc from %v: last error: %v", tryPaths, err)
63+
}
64+
_ = usedPath // retained for potential future diagnostics
65+
66+
doc := string(data)
67+
68+
// For each flag token from help, assert that the doc mentions it.
69+
// We look for the raw token (e.g., "-prompt") to keep this simple and robust to formatting.
70+
for _, token := range flags {
71+
// The version line in help is "--version | -version". Ensure both variants are present in docs.
72+
if token == "--version" {
73+
if !strings.Contains(doc, "--version") || !strings.Contains(doc, "-version") {
74+
t.Fatalf("docs missing one of version tokens: --version or -version; flags=%v", flags)
75+
}
76+
continue
77+
}
78+
// Skip duplicate check for -version since it is covered by the --version case.
79+
if token == "-version" {
80+
continue
81+
}
82+
if !strings.Contains(doc, token) {
83+
t.Fatalf("docs/reference/cli-reference.md missing flag token %q from help; help line present, doc needs update", token)
84+
}
85+
}
86+
}

cmd/agentcli/compat_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"os"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// TestLegacyPrintConfigDefaults_NoNewFlags ensures that a minimal invocation
12+
// without new flags produces the same resolved config baseline.
13+
func TestLegacyPrintConfigDefaults_NoNewFlags(t *testing.T) {
14+
// Ensure env does not influence defaults
15+
t.Setenv("OAI_BASE_URL", "")
16+
t.Setenv("OAI_MODEL", "")
17+
t.Setenv("OAI_HTTP_TIMEOUT", "")
18+
t.Setenv("OAI_IMAGE_MODEL", "")
19+
20+
var out, err bytes.Buffer
21+
code := cliMain([]string{"-prompt", "p", "-print-config"}, &out, &err)
22+
if code != 0 {
23+
t.Fatalf("print-config exit=%d, stderr=%s", code, err.String())
24+
}
25+
// Parse JSON
26+
var payload map[string]any
27+
if jerr := json.Unmarshal(out.Bytes(), &payload); jerr != nil {
28+
t.Fatalf("unmarshal print-config: %v; got %s", jerr, out.String())
29+
}
30+
// Top-level expectations
31+
if got, ok := payload["model"].(string); !ok || got != "oss-gpt-20b" {
32+
t.Fatalf("model=%v; want oss-gpt-20b", payload["model"])
33+
}
34+
if got, ok := payload["baseURL"].(string); !ok || got != "https://api.openai.com/v1" {
35+
t.Fatalf("baseURL=%v; want https://api.openai.com/v1", payload["baseURL"])
36+
}
37+
if got, ok := payload["httpTimeout"].(string); !ok || got != "30s" {
38+
t.Fatalf("httpTimeout=%v; want 30s", payload["httpTimeout"])
39+
}
40+
// Image block expectations
41+
img, ok := payload["image"].(map[string]any)
42+
if !ok {
43+
t.Fatalf("missing image block in print-config")
44+
}
45+
if got, ok := img["model"].(string); !ok || got != "gpt-image-1" {
46+
t.Fatalf("image.model=%v; want gpt-image-1", img["model"])
47+
}
48+
}
49+
50+
func TestConflictingPromptSources_ErrorMessage(t *testing.T) {
51+
var out, err bytes.Buffer
52+
code := cliMain([]string{"-prompt", "p", "-prompt-file", os.DevNull}, &out, &err)
53+
if code != 2 {
54+
t.Fatalf("exit=%d; want 2", code)
55+
}
56+
if !strings.Contains(err.String(), "-prompt and -prompt-file are mutually exclusive") {
57+
t.Fatalf("stderr did not contain conflict message; got: %s", err.String())
58+
}
59+
}
60+
61+
func TestConflictingSystemSources_ErrorMessage(t *testing.T) {
62+
var out, err bytes.Buffer
63+
// Provide both -system (non-default) and -system-file
64+
code := cliMain([]string{"-prompt", "p", "-system", "X", "-system-file", os.DevNull}, &out, &err)
65+
if code != 2 {
66+
t.Fatalf("exit=%d; want 2", code)
67+
}
68+
if !strings.Contains(err.String(), "-system and -system-file are mutually exclusive") {
69+
t.Fatalf("stderr did not contain system conflict message; got: %s", err.String())
70+
}
71+
}
72+
73+
func TestLoadMessagesWithPromptConflict_ErrorMessage(t *testing.T) {
74+
var out, err bytes.Buffer
75+
code := cliMain([]string{"-load-messages", os.DevNull, "-prompt", "p"}, &out, &err)
76+
if code != 2 {
77+
t.Fatalf("exit=%d; want 2", code)
78+
}
79+
if !strings.Contains(err.String(), "-load-messages cannot be combined with -prompt or -prompt-file") {
80+
t.Fatalf("stderr did not contain load/prompt conflict message; got: %s", err.String())
81+
}
82+
}
83+
84+
func TestSaveAndLoadMessagesConflict_ErrorMessage(t *testing.T) {
85+
var out, err bytes.Buffer
86+
code := cliMain([]string{"-prompt", "p", "-save-messages", os.DevNull, "-load-messages", os.DevNull}, &out, &err)
87+
if code != 2 {
88+
t.Fatalf("exit=%d; want 2", code)
89+
}
90+
if !strings.Contains(err.String(), "-save-messages and -load-messages are mutually exclusive") {
91+
t.Fatalf("stderr did not contain save/load conflict message; got: %s", err.String())
92+
}
93+
}

0 commit comments

Comments
 (0)