package main import ( "context" "errors" "os" "path/filepath" "strings" "testing" "time" ) func TestCodexTranslatorAddsTimeout(t *testing.T) { var deadline time.Time translator := &CodexTranslator{ systemPrompt: "Translate from English to Chinese.", thinking: "high", runPrompt: func(ctx context.Context, req codexPromptRequest) (string, error) { var ok bool deadline, ok = ctx.Deadline() if !ok { t.Fatal("expected prompt deadline") } if req.Message != "Translate me" { t.Fatalf("unexpected message %q", req.Message) } if req.Model != defaultOpenAIModel { t.Fatalf("unexpected model %q", req.Model) } if req.Thinking != "high" { t.Fatalf("unexpected thinking %q", req.Thinking) } return "translated", nil }, } got, err := translator.TranslateRaw(context.Background(), "Translate me", "en", "zh-CN") if err != nil { t.Fatalf("TranslateRaw returned error: %v", err) } if got != "translated" { t.Fatalf("unexpected translation %q", got) } remaining := time.Until(deadline) if remaining <= time.Minute || remaining > docsI18nPromptTimeout() { t.Fatalf("unexpected timeout window %s", remaining) } } func TestDocsI18nPromptTimeoutUsesEnvOverride(t *testing.T) { t.Setenv(envDocsI18nPromptTimeout, "5m") if got := docsI18nPromptTimeout(); got != 5*time.Minute { t.Fatalf("expected 5m timeout, got %s", got) } } func TestDocsI18nCommandWaitDelayUsesEnvOverride(t *testing.T) { t.Setenv(envDocsI18nCommandWaitDelay, "50ms") if got := docsI18nCommandWaitDelay(); got != 50*time.Millisecond { t.Fatalf("expected 50ms wait delay, got %s", got) } } func TestIsRetryableTranslateErrorRejectsDeadlineExceeded(t *testing.T) { t.Parallel() if isRetryableTranslateError(context.DeadlineExceeded) { t.Fatal("deadline exceeded should not retry") } } func TestIsRetryableTranslateErrorRejectsAuthenticationFailures(t *testing.T) { t.Parallel() if isRetryableTranslateError(errors.New(`Authentication failed for "openai"`)) { t.Fatal("auth failures should not retry") } if isRetryableTranslateError(errors.New("invalid_api_key")) { t.Fatal("API key failures should not retry") } } func TestIsRetryableTranslateErrorRetriesTransientCodexFailures(t *testing.T) { t.Parallel() for _, message := range []string{ "codex exec failed: rate limit 429", "codex exec failed: stream disconnected", "codex exec failed: 503 temporarily unavailable", } { if !isRetryableTranslateError(errors.New(message)) { t.Fatalf("expected retryable error for %q", message) } } } func TestCodexTranslatorRetriesTransientFailure(t *testing.T) { previousDelay := translateRetryDelay translateRetryDelay = func(int) time.Duration { return 0 } defer func() { translateRetryDelay = previousDelay }() attempts := 0 translator := &CodexTranslator{ systemPrompt: "Translate from English to Chinese.", thinking: "high", runPrompt: func(context.Context, codexPromptRequest) (string, error) { attempts++ if attempts == 1 { return "", errors.New("codex exec failed: stream disconnected") } return "translated", nil }, } got, err := translator.TranslateRaw(context.Background(), "Translate me", "en", "zh-CN") if err != nil { t.Fatalf("TranslateRaw returned error: %v", err) } if got != "translated" { t.Fatalf("unexpected translation %q", got) } if attempts != 2 { t.Fatalf("expected 2 attempts, got %d", attempts) } } func TestCodexTranslatorStripsInputWrapperEcho(t *testing.T) { t.Parallel() translator := &CodexTranslator{ systemPrompt: "Translate from English to German.", thinking: "high", runPrompt: func(context.Context, codexPromptRequest) (string, error) { return "\nÜbersetzt\n", nil }, } got, err := translator.TranslateRaw(context.Background(), "Translate me", "en", "de") if err != nil { t.Fatalf("TranslateRaw returned error: %v", err) } if got != "Übersetzt" { t.Fatalf("unexpected translation %q", got) } } func TestBuildCodexTranslationPromptIncludesGuardrailsAndInput(t *testing.T) { prompt := buildCodexTranslationPrompt("System prompt.", "Hello\nworld") for _, want := range []string{ "System prompt.", "Return only the translated text", "", "Hello\nworld", "", } { if !strings.Contains(prompt, want) { t.Fatalf("expected %q in prompt:\n%s", want, prompt) } } } func TestRunCodexExecPromptUsesOutputLastMessage(t *testing.T) { dir := t.TempDir() fakeCodex := filepath.Join(dir, "codex") if err := os.WriteFile(fakeCodex, []byte(`#!/bin/sh set -eu out="" saw_effort=0 saw_service=0 while [ "$#" -gt 0 ]; do case "$1" in --output-last-message) shift out="$1" ;; -c|--config) shift case "$1" in model_reasoning_effort=\"high\") saw_effort=1 ;; service_tier=\"fast\") saw_service=1 ;; esac ;; esac shift || true done cat >/dev/null if [ "$saw_effort" != "1" ]; then echo "missing high reasoning effort config" >&2 exit 1 fi if [ "$saw_service" != "1" ]; then echo "missing fast service tier config" >&2 exit 1 fi if [ -z "${CODEX_HOME:-}" ]; then echo "missing CODEX_HOME" >&2 exit 1 fi if [ ! -f "$CODEX_HOME/auth.json" ]; then echo "missing auth.json" >&2 exit 1 fi if ! grep -q '"auth_mode":"apikey"' "$CODEX_HOME/auth.json"; then echo "auth.json missing apikey mode" >&2 exit 1 fi if ! grep -q '"OPENAI_API_KEY":"test-openai-key"' "$CODEX_HOME/auth.json"; then echo "auth.json missing API key" >&2 exit 1 fi case "$CODEX_HOME" in /tmp/*) echo "CODEX_HOME must not be under /tmp" >&2 exit 1 ;; esac printf 'translated from codex\n' > "$out" `), 0o755); err != nil { t.Fatalf("write fake codex: %v", err) } t.Setenv(envDocsI18nCodexExecutable, fakeCodex) t.Setenv("OPENAI_API_KEY", "test-openai-key") got, err := runCodexExecPrompt(context.Background(), codexPromptRequest{ SystemPrompt: "Translate.", Message: "Hello", Model: "gpt-5.5", Thinking: "high", }) if err != nil { t.Fatalf("runCodexExecPrompt returned error: %v", err) } if got != "translated from codex" { t.Fatalf("unexpected output %q", got) } } func TestRunCodexExecPromptDoesNotHangOnInheritedPipesAfterTimeout(t *testing.T) { dir := t.TempDir() fakeCodex := filepath.Join(dir, "codex") if err := os.WriteFile(fakeCodex, []byte(`#!/bin/sh set -eu (sleep 10) & sleep 10 `), 0o755); err != nil { t.Fatalf("write fake codex: %v", err) } t.Setenv(envDocsI18nCodexExecutable, fakeCodex) t.Setenv(envDocsI18nCommandWaitDelay, "20ms") t.Setenv("OPENAI_API_KEY", "test-openai-key") ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancel() started := time.Now() _, err := runCodexExecPrompt(ctx, codexPromptRequest{ SystemPrompt: "Translate.", Message: "Hello", Model: "gpt-5.5", Thinking: "high", }) if err == nil { t.Fatal("expected timeout error") } if elapsed := time.Since(started); elapsed > 2*time.Second { t.Fatalf("expected bounded timeout, took %s", elapsed) } } func TestPreviewCommandOutputFlattensAndTruncates(t *testing.T) { input := "line one\n\nline two\tline three " + strings.Repeat("x", 600) preview := previewCommandOutput(input, "") if strings.Contains(preview, "\n") { t.Fatalf("expected flattened whitespace, got %q", preview) } if !strings.HasPrefix(preview, "line one line two line three ") { t.Fatalf("unexpected preview prefix: %q", preview) } if !strings.HasSuffix(preview, "...") { t.Fatalf("expected truncation suffix, got %q", preview) } }