Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
366 lines
9.7 KiB
Go
366 lines
9.7 KiB
Go
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 "<openclaw_docs_i18n_input>\nÜbersetzt\n</openclaw_docs_i18n_input>", 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 TestCodexTranslatorUsesExactGlossaryMatchWithoutPrompt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
translator, err := NewCodexTranslator("en", "zh-CN", []GlossaryEntry{
|
|
{Source: "LINE", Target: "LINE"},
|
|
}, "low")
|
|
if err != nil {
|
|
t.Fatalf("NewCodexTranslator returned error: %v", err)
|
|
}
|
|
translator.runPrompt = func(context.Context, codexPromptRequest) (string, error) {
|
|
t.Fatal("exact glossary matches should not call Codex")
|
|
return "", nil
|
|
}
|
|
|
|
got, err := translator.TranslateRaw(context.Background(), " LINE ", "en", "zh-CN")
|
|
if err != nil {
|
|
t.Fatalf("TranslateRaw returned error: %v", err)
|
|
}
|
|
if got != " LINE " {
|
|
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",
|
|
"<openclaw_docs_i18n_input>",
|
|
"Hello\nworld",
|
|
"</openclaw_docs_i18n_input>",
|
|
} {
|
|
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 TestRunCodexExecPromptUsesOutputLastMessageAfterNonZeroExit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
fakeCodex := filepath.Join(dir, "codex")
|
|
if err := os.WriteFile(fakeCodex, []byte(`#!/bin/sh
|
|
set -eu
|
|
out=""
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--output-last-message)
|
|
shift
|
|
out="$1"
|
|
;;
|
|
esac
|
|
shift || true
|
|
done
|
|
cat >/dev/null
|
|
printf 'translated despite nonzero\n' > "$out"
|
|
echo "transient Codex shutdown failure" >&2
|
|
exit 1
|
|
`), 0o755); err != nil {
|
|
t.Fatalf("write fake codex: %v", err)
|
|
}
|
|
t.Setenv(envDocsI18nCodexExecutable, fakeCodex)
|
|
|
|
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 despite nonzero" {
|
|
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", 1200) + " final api error 429"
|
|
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.Contains(preview, "... [truncated] ...") {
|
|
t.Fatalf("expected truncation marker, got %q", preview)
|
|
}
|
|
if !strings.HasSuffix(preview, "final api error 429") {
|
|
t.Fatalf("expected retained error tail, got %q", preview)
|
|
}
|
|
}
|
|
|
|
func TestPreviewCommandOutputRetainsStderrTail(t *testing.T) {
|
|
stdout := "startup banner " + strings.Repeat("x", 1200)
|
|
stderr := "provider api error 429"
|
|
preview := previewCommandOutput(stdout, stderr)
|
|
if !strings.HasPrefix(preview, "startup banner ") {
|
|
t.Fatalf("unexpected preview prefix: %q", preview)
|
|
}
|
|
if !strings.HasSuffix(preview, stderr) {
|
|
t.Fatalf("expected retained stderr tail, got %q", preview)
|
|
}
|
|
}
|