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
279 lines
7.3 KiB
Go
279 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type behaviorReplacePair struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
type behaviorRule struct {
|
|
Method string `json:"method"`
|
|
MatchAll []string `json:"match_all"`
|
|
ResponseFile string `json:"response_file,omitempty"`
|
|
ReplacePairs []behaviorReplacePair `json:"replace_pairs,omitempty"`
|
|
}
|
|
|
|
type behaviorFixture struct {
|
|
Name string `json:"name"`
|
|
Mode string `json:"mode"`
|
|
RelPath string `json:"rel_path"`
|
|
SourceFile string `json:"source_file"`
|
|
ExpectedFile string `json:"expected_file,omitempty"`
|
|
ExpectedErrorContains string `json:"expected_error_contains,omitempty"`
|
|
ExpectedOutputContains []string `json:"expected_output_contains,omitempty"`
|
|
ExpectedOutputNotContains []string `json:"expected_output_not_contains,omitempty"`
|
|
Rules []behaviorRule `json:"rules"`
|
|
}
|
|
|
|
type behaviorFixtureTranslator struct {
|
|
t *testing.T
|
|
dir string
|
|
rules []behaviorRule
|
|
}
|
|
|
|
func (tr *behaviorFixtureTranslator) Translate(_ context.Context, text, _, _ string) (string, error) {
|
|
return tr.run("masked", text), nil
|
|
}
|
|
|
|
func (tr *behaviorFixtureTranslator) TranslateRaw(_ context.Context, text, _, _ string) (string, error) {
|
|
return tr.run("raw", text), nil
|
|
}
|
|
|
|
func (tr *behaviorFixtureTranslator) Close() {}
|
|
|
|
func (tr *behaviorFixtureTranslator) run(method, text string) string {
|
|
tr.t.Helper()
|
|
for _, rule := range tr.rules {
|
|
if rule.Method != method {
|
|
continue
|
|
}
|
|
if !matchesAll(text, rule.MatchAll) {
|
|
continue
|
|
}
|
|
switch {
|
|
case rule.ResponseFile != "":
|
|
return readFixtureTextInDir(tr.t, tr.dir, rule.ResponseFile)
|
|
case len(rule.ReplacePairs) > 0:
|
|
out := text
|
|
for _, pair := range rule.ReplacePairs {
|
|
out = strings.ReplaceAll(out, pair.From, pair.To)
|
|
}
|
|
return out
|
|
default:
|
|
return text
|
|
}
|
|
}
|
|
return text
|
|
}
|
|
|
|
func matchesAll(text string, fragments []string) bool {
|
|
for _, fragment := range fragments {
|
|
if !strings.Contains(text, fragment) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestDocsI18nBehaviorBaselines(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := filepath.Join("testdata", "behavior")
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
t.Fatalf("ReadDir(%q): %v", root, err)
|
|
}
|
|
|
|
found := false
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
found = true
|
|
dir := filepath.Join(root, entry.Name())
|
|
fixture := loadBehaviorFixture(t, dir)
|
|
name := fixture.Name
|
|
if name == "" {
|
|
name = entry.Name()
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
runBehaviorFixture(t, dir, fixture)
|
|
})
|
|
}
|
|
|
|
if !found {
|
|
t.Fatalf("no behavior fixtures found under %s", root)
|
|
}
|
|
}
|
|
|
|
func loadBehaviorFixture(t *testing.T, dir string) behaviorFixture {
|
|
t.Helper()
|
|
data, err := os.ReadFile(filepath.Join(dir, "case.json"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(case.json): %v", err)
|
|
}
|
|
var fixture behaviorFixture
|
|
if err := json.Unmarshal(data, &fixture); err != nil {
|
|
t.Fatalf("Unmarshal(case.json): %v", err)
|
|
}
|
|
return fixture
|
|
}
|
|
|
|
func runBehaviorFixture(t *testing.T, dir string, fixture behaviorFixture) {
|
|
t.Helper()
|
|
|
|
source := readFixtureTextInDir(t, dir, fixture.SourceFile)
|
|
translator := &behaviorFixtureTranslator{
|
|
t: t,
|
|
dir: dir,
|
|
rules: fixture.Rules,
|
|
}
|
|
|
|
var (
|
|
got string
|
|
err error
|
|
)
|
|
|
|
switch fixture.Mode {
|
|
case "doc_body_chunked":
|
|
got, err = translateDocBodyChunked(context.Background(), translator, fixture.RelPath, source, "en", "zh-CN")
|
|
case "frontmatter_scalar":
|
|
got, err = translateSnippet(
|
|
context.Background(),
|
|
translator,
|
|
&TranslationMemory{entries: map[string]TMEntry{}},
|
|
fixture.RelPath+":frontmatter:title",
|
|
source,
|
|
"en",
|
|
"zh-CN",
|
|
)
|
|
default:
|
|
t.Fatalf("unsupported fixture mode %q", fixture.Mode)
|
|
}
|
|
|
|
if fixture.ExpectedErrorContains != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", fixture.ExpectedErrorContains)
|
|
}
|
|
if !strings.Contains(err.Error(), fixture.ExpectedErrorContains) {
|
|
t.Fatalf("expected error containing %q, got %v", fixture.ExpectedErrorContains, err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if fixture.ExpectedFile != "" {
|
|
want := readFixtureTextInDir(t, dir, fixture.ExpectedFile)
|
|
if normalizeBehaviorText(got) != normalizeBehaviorText(want) {
|
|
t.Fatalf("unexpected output\nwant:\n%s\n\ngot:\n%s", want, got)
|
|
}
|
|
}
|
|
|
|
for _, fragment := range fixture.ExpectedOutputContains {
|
|
if !strings.Contains(got, fragment) {
|
|
t.Fatalf("expected output to contain %q\noutput:\n%s", fragment, got)
|
|
}
|
|
}
|
|
for _, fragment := range fixture.ExpectedOutputNotContains {
|
|
if strings.Contains(got, fragment) {
|
|
t.Fatalf("expected output to exclude %q\noutput:\n%s", fragment, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func readFixtureText(t *testing.T, path string) string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(%q): %v", path, err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func readFixtureTextInDir(t *testing.T, dir, name string) string {
|
|
t.Helper()
|
|
resolvedPath, err := resolveFixturePathInDir(dir, name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return readFixtureText(t, resolvedPath)
|
|
}
|
|
|
|
func resolveFixturePathInDir(dir, name string) (string, error) {
|
|
if filepath.IsAbs(name) {
|
|
return "", fmt.Errorf("absolute fixture paths are not allowed: %q", name)
|
|
}
|
|
clean := filepath.Clean(name)
|
|
if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
|
|
return "", fmt.Errorf("fixture path escapes dir: %q", name)
|
|
}
|
|
|
|
joined := filepath.Join(dir, clean)
|
|
resolvedDir, err := filepath.EvalSymlinks(dir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("EvalSymlinks(%q): %w", dir, err)
|
|
}
|
|
resolvedPath, err := filepath.EvalSymlinks(joined)
|
|
if err != nil {
|
|
return "", fmt.Errorf("EvalSymlinks(%q): %w", joined, err)
|
|
}
|
|
|
|
rel, err := filepath.Rel(resolvedDir, resolvedPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Rel(%q, %q): %w", resolvedDir, resolvedPath, err)
|
|
}
|
|
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "", fmt.Errorf("fixture path resolves outside dir: %q", name)
|
|
}
|
|
|
|
return resolvedPath, nil
|
|
}
|
|
|
|
func normalizeBehaviorText(value string) string {
|
|
return strings.TrimSpace(strings.ReplaceAll(value, "\r\n", "\n"))
|
|
}
|
|
|
|
func TestResolveFixturePathInDirRejectsSymlinkEscape(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := t.TempDir()
|
|
fixtureDir := filepath.Join(root, "fixture")
|
|
if err := os.MkdirAll(fixtureDir, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(%q): %v", fixtureDir, err)
|
|
}
|
|
|
|
outsidePath := filepath.Join(root, "outside.txt")
|
|
if err := os.WriteFile(outsidePath, []byte("outside\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(%q): %v", outsidePath, err)
|
|
}
|
|
|
|
linkPath := filepath.Join(fixtureDir, "outside-link.txt")
|
|
if err := os.Symlink(outsidePath, linkPath); err != nil {
|
|
if os.IsPermission(err) || runtime.GOOS == "windows" {
|
|
t.Skipf("symlink creation unavailable in this test environment: %v", err)
|
|
}
|
|
t.Fatalf("Symlink(%q, %q): %v", outsidePath, linkPath, err)
|
|
}
|
|
|
|
_, err := resolveFixturePathInDir(fixtureDir, "outside-link.txt")
|
|
if err == nil {
|
|
t.Fatal("expected symlink escape to fail")
|
|
}
|
|
if !strings.Contains(err.Error(), "resolves outside dir") {
|
|
t.Fatalf("expected outside-dir error, got %v", err)
|
|
}
|
|
}
|