chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
package codex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zzet/gortex/internal/llm"
|
||||
)
|
||||
|
||||
// fakeCodex writes a shell script that impersonates the `codex` CLI.
|
||||
// It logs args + stdin to sidecar files for assertions, writes
|
||||
// opts.lastMessage to whatever path follows --output-last-message
|
||||
// (mirroring real `codex exec`), echoes opts.stdout as progress noise,
|
||||
// and exits with opts.exitCode.
|
||||
//
|
||||
// A shell-script fake exercises every provider code path — arg
|
||||
// construction, stdin piping, the --output-last-message sidecar, the
|
||||
// stdout fallback, exit-status and stderr handling — without a real
|
||||
// codex install.
|
||||
func fakeCodex(t *testing.T, dir string, opts fakeOpts) string {
|
||||
t.Helper()
|
||||
if dir == "" {
|
||||
dir = t.TempDir()
|
||||
}
|
||||
script := filepath.Join(dir, "fake-codex.sh")
|
||||
|
||||
argsLog := filepath.Join(dir, "args.txt")
|
||||
stdinLog := filepath.Join(dir, "stdin.txt")
|
||||
q := func(s string) string { return strings.ReplaceAll(s, "'", "'\\''") }
|
||||
|
||||
body := "#!/bin/sh\n" +
|
||||
"printf '%s\\n' \"$@\" > '" + argsLog + "'\n" +
|
||||
"cat > '" + stdinLog + "'\n" +
|
||||
"prev=''\n" +
|
||||
"outfile=''\n" +
|
||||
"for a in \"$@\"; do\n" +
|
||||
" if [ \"$prev\" = '--output-last-message' ]; then outfile=\"$a\"; fi\n" +
|
||||
" prev=\"$a\"\n" +
|
||||
"done\n"
|
||||
if opts.sleep > 0 {
|
||||
body += fmt.Sprintf("sleep %d\n", int(opts.sleep.Seconds()+1))
|
||||
}
|
||||
if opts.lastMessage != "" {
|
||||
body += "if [ -n \"$outfile\" ]; then printf '%s' '" + q(opts.lastMessage) + "' > \"$outfile\"; fi\n"
|
||||
}
|
||||
if opts.stderr != "" {
|
||||
body += "printf '%s' '" + q(opts.stderr) + "' >&2\n"
|
||||
}
|
||||
if opts.stdout != "" {
|
||||
body += "printf '%s' '" + q(opts.stdout) + "'\n"
|
||||
}
|
||||
if opts.exitCode != 0 {
|
||||
body += fmt.Sprintf("exit %d\n", opts.exitCode)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(script, []byte(body), 0o700); err != nil {
|
||||
t.Fatalf("write fake script: %v", err)
|
||||
}
|
||||
return script
|
||||
}
|
||||
|
||||
type fakeOpts struct {
|
||||
lastMessage string // written to the --output-last-message sidecar
|
||||
stdout string // emitted on stdout (progress logs)
|
||||
stderr string
|
||||
exitCode int
|
||||
sleep time.Duration
|
||||
}
|
||||
|
||||
func readSidecar(t *testing.T, scriptPath, name string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(filepath.Join(filepath.Dir(scriptPath), name))
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", name, err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestNew_BinaryNotFound(t *testing.T) {
|
||||
if _, err := New(llm.CodexConfig{Binary: "codex-nonexistent-zzzzz"}); err == nil {
|
||||
t.Fatal("expected error when binary is not on PATH")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_DefaultsBinary(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "codex")
|
||||
if err := os.WriteFile(bin, []byte("#!/bin/sh\nexit 0\n"), 0o700); err != nil {
|
||||
t.Fatalf("write fake binary: %v", err)
|
||||
}
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
p, err := New(llm.CodexConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
defer p.Close()
|
||||
if p.Name() != "codex" {
|
||||
t.Errorf("Name()=%q want codex", p.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_FreeformSuccess(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{lastMessage: "hello world", stdout: "[progress] thinking…"})
|
||||
|
||||
p, err := New(llm.CodexConfig{Binary: script})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
resp, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{
|
||||
{Role: llm.RoleSystem, Content: "be terse"},
|
||||
{Role: llm.RoleUser, Content: "hi"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Complete: %v", err)
|
||||
}
|
||||
if resp.Text != "hello world" {
|
||||
t.Errorf("text=%q want hello world (the sidecar must win over stdout noise)", resp.Text)
|
||||
}
|
||||
|
||||
gotArgs := readSidecar(t, script, "args.txt")
|
||||
for _, want := range []string{"exec", "--skip-git-repo-check", "--sandbox", "read-only", "--output-last-message", "-"} {
|
||||
if !strings.Contains(gotArgs, want) {
|
||||
t.Errorf("args missing %q\nargs=\n%s", want, gotArgs)
|
||||
}
|
||||
}
|
||||
stdin := readSidecar(t, script, "stdin.txt")
|
||||
if !strings.Contains(stdin, "User: hi") {
|
||||
t.Errorf("stdin missing user turn:\n%s", stdin)
|
||||
}
|
||||
if !strings.Contains(stdin, "System instructions:") || !strings.Contains(stdin, "be terse") {
|
||||
t.Errorf("system content must be folded into the prompt:\n%s", stdin)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_StdoutFallback(t *testing.T) {
|
||||
// No sidecar payload — the provider must fall back to stdout for
|
||||
// an older codex build that does not honour --output-last-message.
|
||||
script := fakeCodex(t, "", fakeOpts{stdout: "fallback answer"})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script})
|
||||
defer p.Close()
|
||||
|
||||
resp, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Complete: %v", err)
|
||||
}
|
||||
if resp.Text != "fallback answer" {
|
||||
t.Errorf("text=%q want fallback answer", resp.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_PassesModel(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{lastMessage: "ok"})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script, Model: "gpt-5-codex"})
|
||||
defer p.Close()
|
||||
|
||||
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
|
||||
}); err != nil {
|
||||
t.Fatalf("Complete: %v", err)
|
||||
}
|
||||
args := readSidecar(t, script, "args.txt")
|
||||
if !strings.Contains(args, "--model\ngpt-5-codex") {
|
||||
t.Errorf("args missing --model gpt-5-codex:\n%s", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_StructuredExtractsJSON(t *testing.T) {
|
||||
wrapped := "```json\n{\"terms\":[\"bcrypt\",\"argon2\"]}\n```\n"
|
||||
script := fakeCodex(t, "", fakeOpts{lastMessage: wrapped})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script})
|
||||
defer p.Close()
|
||||
|
||||
resp, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "expand 'password hashing'"}},
|
||||
Shape: llm.ShapeExpandTerms,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Complete: %v", err)
|
||||
}
|
||||
if resp.Text != `{"terms":["bcrypt","argon2"]}` {
|
||||
t.Errorf("text=%q want the unwrapped JSON object", resp.Text)
|
||||
}
|
||||
stdin := readSidecar(t, script, "stdin.txt")
|
||||
if !strings.Contains(stdin, "JSON Schema") {
|
||||
t.Errorf("structured request must inject a JSON Schema rider; stdin=\n%s", stdin)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_StructuredNoJSONErrors(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{lastMessage: "I cannot help with that."})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script})
|
||||
defer p.Close()
|
||||
|
||||
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "x"}},
|
||||
Shape: llm.ShapeExpandTerms,
|
||||
}); err == nil {
|
||||
t.Fatal("expected error when structured response carried no JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_NonZeroExit(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{exitCode: 2, stderr: "not signed in"})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script})
|
||||
defer p.Close()
|
||||
|
||||
_, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-zero exit")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not signed in") {
|
||||
t.Errorf("error should include stderr snippet; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_EmptyResponseErrors(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script})
|
||||
defer p.Close()
|
||||
|
||||
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
|
||||
}); err == nil {
|
||||
t.Fatal("expected error for empty response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_ContextCancellation(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{lastMessage: "late", sleep: 2 * time.Second})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script, TimeoutSeconds: 1})
|
||||
defer p.Close()
|
||||
|
||||
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
|
||||
}); err == nil {
|
||||
t.Fatal("expected timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete_ExtraArgsForwarded(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{lastMessage: "ok"})
|
||||
|
||||
p, _ := New(llm.CodexConfig{Binary: script, Args: []string{"--sandbox", "workspace-write"}})
|
||||
defer p.Close()
|
||||
|
||||
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
|
||||
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
|
||||
}); err != nil {
|
||||
t.Fatalf("Complete: %v", err)
|
||||
}
|
||||
args := readSidecar(t, script, "args.txt")
|
||||
if !strings.Contains(args, "workspace-write") {
|
||||
t.Errorf("args missing forwarded extra arg:\n%s", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrompt_Roles(t *testing.T) {
|
||||
prompt := buildPrompt([]llm.Message{
|
||||
{Role: llm.RoleSystem, Content: "rule 1"},
|
||||
{Role: llm.RoleSystem, Content: "rule 2"},
|
||||
{Role: llm.RoleUser, Content: "q1"},
|
||||
{Role: llm.RoleAssistant, Content: "a1"},
|
||||
{Role: llm.RoleTool, Content: "[1,2,3]", ToolName: "search_symbols"},
|
||||
{Role: llm.RoleUser, Content: "q2"},
|
||||
})
|
||||
if !strings.HasPrefix(prompt, "System instructions:\nrule 1\n\nrule 2") {
|
||||
t.Errorf("system block must lead the prompt:\n%s", prompt)
|
||||
}
|
||||
for _, want := range []string{"User: q1", "Assistant: a1", "Tool result (search_symbols)", "User: q2"} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Errorf("prompt missing %q\nprompt=\n%s", want, prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrompt_NoSystem(t *testing.T) {
|
||||
prompt := buildPrompt([]llm.Message{{Role: llm.RoleUser, Content: "q"}})
|
||||
if strings.Contains(prompt, "System instructions:") {
|
||||
t.Errorf("no system turns → no system block:\n%s", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check: the test helper itself can run a child process.
|
||||
func TestHelper_FakeScriptIsExecutable(t *testing.T) {
|
||||
script := fakeCodex(t, "", fakeOpts{stdout: "alive"})
|
||||
cmd := exec.Command(script, "ping")
|
||||
cmd.Stdin = strings.NewReader("")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Skipf("cannot exec fake script: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(out), "alive") {
|
||||
t.Errorf("output=%q", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user