docs-i18n: add behavior baseline fixtures

This commit is contained in:
masonxhuang
2026-04-10 12:17:49 +08:00
committed by Mason Huang
parent 732db75279
commit 89690e6c5a
19 changed files with 338 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"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 (t *behaviorFixtureTranslator) Translate(_ context.Context, text, _, _ string) (string, error) {
return t.run("masked", text), nil
}
func (t *behaviorFixtureTranslator) TranslateRaw(_ context.Context, text, _, _ string) (string, error) {
return t.run("raw", text), nil
}
func (t *behaviorFixtureTranslator) Close() {}
func (t *behaviorFixtureTranslator) run(method, text string) string {
t.t.Helper()
for _, rule := range t.rules {
if rule.Method != method {
continue
}
if !matchesAll(text, rule.MatchAll) {
continue
}
switch {
case rule.ResponseFile != "":
return readFixtureText(t.t, filepath.Join(t.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 := readFixtureText(t, filepath.Join(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 := readFixtureText(t, filepath.Join(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 normalizeBehaviorText(value string) string {
return strings.TrimSpace(strings.ReplaceAll(value, "\r\n", "\n"))
}

View File

@@ -0,0 +1,24 @@
{
"name": "fenced singleton retries after malformed raw output",
"mode": "doc_body_chunked",
"rel_path": "gateway/configuration-reference.md",
"source_file": "source.txt",
"expected_output_contains": [
"Translated line 01",
"Translated line 02",
"Translated line 03",
"Translated line 04"
],
"expected_output_not_contains": ["Line 01", "Line 02", "Line 03", "Line 04"],
"rules": [
{
"method": "raw",
"match_all": ["Line 01", "Line 04"],
"response_file": "raw-malformed.txt"
},
{
"method": "raw",
"replace_pairs": [{ "from": "Line ", "to": "Translated line " }]
}
]
}

View File

@@ -0,0 +1,7 @@
```md
Translated line 01
Translated line 02
Translated line 03
Translated line 04
```

View File

@@ -0,0 +1,6 @@
```md
Line 01
Line 02
Line 03
Line 04

View File

@@ -0,0 +1,7 @@
```md
Line 01
Line 02
Line 03
Line 04
```

View File

@@ -0,0 +1,14 @@
{
"name": "frontmatter scalar falls back instead of keeping tagged wrapper",
"mode": "frontmatter_scalar",
"rel_path": "install/fly.md",
"source_file": "source.txt",
"expected_file": "expected.txt",
"rules": [
{
"method": "masked",
"match_all": ["Deploying OpenClaw on Fly.io"],
"response_file": "masked-tagged-wrapper.txt"
}
]
}

View File

@@ -0,0 +1 @@
Deploying OpenClaw on Fly.io

View File

@@ -0,0 +1,7 @@
<frontmatter>
title: Fly.io
</frontmatter>
<body>
# Fly.io 部署
</body>

View File

@@ -0,0 +1 @@
Deploying OpenClaw on Fly.io

View File

@@ -0,0 +1,25 @@
{
"name": "protocol leak retries on smaller chunks",
"mode": "doc_body_chunked",
"rel_path": "gateway/configuration-reference.md",
"source_file": "source.txt",
"expected_file": "expected.txt",
"expected_output_not_contains": ["<frontmatter>", "<body>", "[[[FM_"],
"rules": [
{
"method": "raw",
"match_all": ["First chunk", "Second chunk"],
"response_file": "raw-leaked.txt"
},
{
"method": "raw",
"match_all": ["First chunk"],
"response_file": "raw-first.txt"
},
{
"method": "raw",
"match_all": ["Second chunk"],
"response_file": "raw-second.txt"
}
]
}

View File

@@ -0,0 +1,4 @@
First translated
Second translated

View File

@@ -0,0 +1,2 @@
First translated

View File

@@ -0,0 +1,9 @@
<frontmatter>
title: leaked
</frontmatter>
<body>
First translated
Second translated
</body>

View File

@@ -0,0 +1,2 @@
Second translated

View File

@@ -0,0 +1,4 @@
First chunk
Second chunk

View File

@@ -0,0 +1,14 @@
{
"name": "uppercase body wrapper is stripped",
"mode": "doc_body_chunked",
"rel_path": "help/testing.md",
"source_file": "source.txt",
"expected_file": "expected.txt",
"rules": [
{
"method": "raw",
"match_all": ["Regular paragraph."],
"response_file": "raw-uppercase-wrapper.txt"
}
]
}

View File

@@ -0,0 +1 @@
Translated paragraph.

View File

@@ -0,0 +1,3 @@
<BODY>
Translated paragraph.
</BODY>

View File

@@ -0,0 +1 @@
Regular paragraph.