mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 13:30:46 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
360
openclaw/scripts/docs-i18n/translator_test.go
Normal file
360
openclaw/scripts/docs-i18n/translator_test.go
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakePromptRunner struct {
|
||||
prompt func(context.Context, string) (string, error)
|
||||
stderr string
|
||||
}
|
||||
|
||||
func (runner fakePromptRunner) Prompt(ctx context.Context, message string) (string, error) {
|
||||
return runner.prompt(ctx, message)
|
||||
}
|
||||
|
||||
func (runner fakePromptRunner) Stderr() string {
|
||||
return runner.stderr
|
||||
}
|
||||
|
||||
type fakePiPromptClient struct {
|
||||
prompt func(context.Context, string) (string, error)
|
||||
stderr string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (client *fakePiPromptClient) Prompt(ctx context.Context, message string) (string, error) {
|
||||
return client.prompt(ctx, message)
|
||||
}
|
||||
|
||||
func (client *fakePiPromptClient) Stderr() string {
|
||||
return client.stderr
|
||||
}
|
||||
|
||||
func (client *fakePiPromptClient) Close() error {
|
||||
client.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRunPromptAddsTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var deadline time.Time
|
||||
client := fakePromptRunner{
|
||||
prompt: func(ctx context.Context, message string) (string, error) {
|
||||
var ok bool
|
||||
deadline, ok = ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("expected prompt deadline")
|
||||
}
|
||||
if message != "Translate me" {
|
||||
t.Fatalf("unexpected message %q", message)
|
||||
}
|
||||
return "translated", nil
|
||||
},
|
||||
}
|
||||
|
||||
got, err := runPrompt(context.Background(), client, "Translate me")
|
||||
if err != nil {
|
||||
t.Fatalf("runPrompt 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 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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRetryableTranslateErrorRetriesPiTermination(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !isRetryableTranslateError(errors.New("pi error: terminated; stopReason=error; assistant=partial output")) {
|
||||
t.Fatal("terminated pi session should retry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRetryableTranslateErrorRetriesTerminatedStopReason(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !isRetryableTranslateError(errors.New("pi error: stopReason=terminated; assistant=partial output")) {
|
||||
t.Fatal("terminated stopReason should retry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRetryableTranslateErrorRetriesCanceledStopReasons(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, message := range []string{
|
||||
"pi error: stopReason=cancelled; assistant=partial output",
|
||||
"pi error: stopReason=canceled; assistant=partial output",
|
||||
"pi error: stopReason=aborted; assistant=partial output",
|
||||
} {
|
||||
if !isRetryableTranslateError(errors.New(message)) {
|
||||
t.Fatalf("expected retryable stop reason for %q", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPromptIncludesStderr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootErr := errors.New("context deadline exceeded")
|
||||
client := fakePromptRunner{
|
||||
prompt: func(context.Context, string) (string, error) {
|
||||
return "", rootErr
|
||||
},
|
||||
stderr: "boom",
|
||||
}
|
||||
|
||||
_, err := runPrompt(context.Background(), client, "Translate me")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !errors.Is(err, rootErr) {
|
||||
t.Fatalf("expected wrapped root error, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "pi stderr: boom") {
|
||||
t.Fatalf("expected stderr in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoratePromptErrorLeavesCleanErrorsAlone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootErr := errors.New("plain failure")
|
||||
got := decoratePromptError(rootErr, " ")
|
||||
if !errors.Is(got, rootErr) {
|
||||
t.Fatalf("expected original error, got %v", got)
|
||||
}
|
||||
if got.Error() != rootErr.Error() {
|
||||
t.Fatalf("expected unchanged message, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDocsPiCommandUsesOverrideEnv(t *testing.T) {
|
||||
t.Setenv(envDocsPiExecutable, "/tmp/custom-pi")
|
||||
t.Setenv(envDocsPiArgs, "--mode rpc --foo bar")
|
||||
|
||||
command, err := resolveDocsPiCommand(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("resolveDocsPiCommand returned error: %v", err)
|
||||
}
|
||||
|
||||
if command.Executable != "/tmp/custom-pi" {
|
||||
t.Fatalf("unexpected executable %q", command.Executable)
|
||||
}
|
||||
if strings.Join(command.Args, " ") != "--mode rpc --foo bar" {
|
||||
t.Fatalf("unexpected args %v", command.Args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsPiModelRefUsesProviderPrefixWhenProviderFlagIsOmitted(t *testing.T) {
|
||||
t.Setenv(envDocsI18nProvider, "openai")
|
||||
t.Setenv(envDocsI18nModel, "gpt-5.4")
|
||||
t.Setenv(envDocsPiOmitProvider, "1")
|
||||
|
||||
if got := docsPiProviderArg(); got != "" {
|
||||
t.Fatalf("expected empty provider arg when omit-provider is enabled, got %q", got)
|
||||
}
|
||||
if got := docsPiModelRef(); got != "openai/gpt-5.4" {
|
||||
t.Fatalf("expected provider-qualified model ref, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldMaterializePiRuntimeForPiMonoWrapper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "Projects", "pi-mono", "packages", "coding-agent", "dist")
|
||||
binDir := filepath.Join(root, "bin")
|
||||
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir source dir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir bin dir: %v", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(sourceDir, "cli.js")
|
||||
if err := os.WriteFile(target, []byte("console.log('pi');\n"), 0o644); err != nil {
|
||||
t.Fatalf("write target: %v", err)
|
||||
}
|
||||
link := filepath.Join(binDir, "pi")
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatalf("symlink: %v", err)
|
||||
}
|
||||
|
||||
if !shouldMaterializePiRuntime(link) {
|
||||
t.Fatal("expected pi-mono wrapper to materialize runtime")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiTranslatorRestartsClientAfterPiTermination(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clients := []*fakePiPromptClient{}
|
||||
factoryCalls := 0
|
||||
factory := func(context.Context) (docsPiPromptClient, error) {
|
||||
factoryCalls++
|
||||
index := factoryCalls
|
||||
client := &fakePiPromptClient{
|
||||
prompt: func(context.Context, string) (string, error) {
|
||||
if index == 1 {
|
||||
return "", errors.New("pi error: terminated; stopReason=error; assistant=partial output")
|
||||
}
|
||||
return "translated", nil
|
||||
},
|
||||
}
|
||||
clients = append(clients, client)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client, err := factory(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("factory failed: %v", err)
|
||||
}
|
||||
translator := &PiTranslator{client: client, clientFactory: factory}
|
||||
|
||||
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 factoryCalls != 2 {
|
||||
t.Fatalf("expected factory to run twice, got %d", factoryCalls)
|
||||
}
|
||||
if len(clients) != 2 {
|
||||
t.Fatalf("expected 2 clients, got %d", len(clients))
|
||||
}
|
||||
if !clients[0].closed {
|
||||
t.Fatal("expected first client to close before retry")
|
||||
}
|
||||
if clients[1].closed {
|
||||
t.Fatal("expected replacement client to remain open")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiTranslatorRestartsClientAfterTerminatedStopReason(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clients := []*fakePiPromptClient{}
|
||||
factoryCalls := 0
|
||||
factory := func(context.Context) (docsPiPromptClient, error) {
|
||||
factoryCalls++
|
||||
index := factoryCalls
|
||||
client := &fakePiPromptClient{
|
||||
prompt: func(context.Context, string) (string, error) {
|
||||
if index == 1 {
|
||||
return "", errors.New("pi error: stopReason=terminated; assistant=partial output")
|
||||
}
|
||||
return "translated", nil
|
||||
},
|
||||
}
|
||||
clients = append(clients, client)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client, err := factory(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("factory failed: %v", err)
|
||||
}
|
||||
translator := &PiTranslator{client: client, clientFactory: factory}
|
||||
|
||||
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 factoryCalls != 2 {
|
||||
t.Fatalf("expected factory to run twice, got %d", factoryCalls)
|
||||
}
|
||||
if len(clients) != 2 {
|
||||
t.Fatalf("expected 2 clients, got %d", len(clients))
|
||||
}
|
||||
if !clients[0].closed {
|
||||
t.Fatal("expected first client to close before retry")
|
||||
}
|
||||
if clients[1].closed {
|
||||
t.Fatal("expected replacement client to remain open")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiTranslatorRestartsClientAfterCanceledStopReason(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clients := []*fakePiPromptClient{}
|
||||
factoryCalls := 0
|
||||
factory := func(context.Context) (docsPiPromptClient, error) {
|
||||
factoryCalls++
|
||||
index := factoryCalls
|
||||
client := &fakePiPromptClient{
|
||||
prompt: func(context.Context, string) (string, error) {
|
||||
if index == 1 {
|
||||
return "", errors.New("pi error: stopReason=aborted; assistant=partial output")
|
||||
}
|
||||
return "translated", nil
|
||||
},
|
||||
}
|
||||
clients = append(clients, client)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client, err := factory(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("factory failed: %v", err)
|
||||
}
|
||||
translator := &PiTranslator{client: client, clientFactory: factory}
|
||||
|
||||
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 factoryCalls != 2 {
|
||||
t.Fatalf("expected factory to run twice, got %d", factoryCalls)
|
||||
}
|
||||
if !clients[0].closed {
|
||||
t.Fatal("expected first client to close before retry")
|
||||
}
|
||||
if clients[1].closed {
|
||||
t.Fatal("expected replacement client to remain open")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue