mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
893 lines
27 KiB
Go
893 lines
27 KiB
Go
package backendrpc
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type testRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
}
|
|
|
|
type testEnvelope struct {
|
|
OK bool `json:"ok"`
|
|
Status string `json:"status"`
|
|
Data any `json:"data,omitempty"`
|
|
Error any `json:"error,omitempty"`
|
|
RequestID string `json:"request_id"`
|
|
}
|
|
|
|
type testResponse struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id"`
|
|
Result any `json:"result,omitempty"`
|
|
Error *JSONRPCError `json:"error,omitempty"`
|
|
}
|
|
|
|
func newTestClient(t *testing.T, handler func(t *testing.T, req testRequest) testResponse) *Client {
|
|
t.Helper()
|
|
client := New("bat.sock")
|
|
client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
|
|
clientConn, serverConn := net.Pipe()
|
|
go func(conn net.Conn) {
|
|
defer conn.Close()
|
|
line, err := bufio.NewReader(conn).ReadBytes('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
var req testRequest
|
|
if err := json.Unmarshal(line, &req); err != nil {
|
|
return
|
|
}
|
|
resp := handler(t, req)
|
|
if len(resp.ID) == 0 {
|
|
resp.ID = req.ID
|
|
}
|
|
if resp.JSONRPC == "" {
|
|
resp.JSONRPC = jsonRPCVersion
|
|
}
|
|
_ = json.NewEncoder(conn).Encode(resp)
|
|
}(serverConn)
|
|
return clientConn, nil
|
|
}
|
|
return client
|
|
}
|
|
|
|
func TestResourceRepairQueuesTask(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "resource.repair" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "accepted",
|
|
RequestID: "req-test-1",
|
|
Data: map[string]any{
|
|
"task_id": "task-1",
|
|
"kind": "resource.repair",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
task, err := client.ResourceRepair(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ResourceRepair error: %v", err)
|
|
}
|
|
if task.TaskID != "task-1" || task.Kind != "resource.repair" {
|
|
t.Fatalf("unexpected task: %#v", task)
|
|
}
|
|
}
|
|
|
|
func TestDaemonRestartSendsControlMethod(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "daemon.restart" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "accepted",
|
|
RequestID: "req-test-restart",
|
|
Data: map[string]any{
|
|
"command": "restart",
|
|
"status": "accepted",
|
|
"message": "restart accepted",
|
|
"state_dir": "/tmp/bat-pid",
|
|
"socket_path": "/tmp/bat-pid/bat.sock",
|
|
"controller_pid": 4242,
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
ack, err := client.DaemonRestart(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("DaemonRestart error: %v", err)
|
|
}
|
|
if ack.Command != "restart" || ack.ControllerPID == nil || *ack.ControllerPID != 4242 {
|
|
t.Fatalf("unexpected ack: %#v", ack)
|
|
}
|
|
}
|
|
|
|
func TestParseTextUnitsSendsQuery(t *testing.T) {
|
|
pathID := int64(7)
|
|
classID := 114
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "parse.text_units" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params TextUnitQueryParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.Offset != 3 || params.Limit != 5 || params.Destination != "*Table*" || params.PathID == nil || *params.PathID != pathID || params.ClassID == nil || *params.ClassID != classID {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-parse",
|
|
Data: map[string]any{
|
|
"available": true,
|
|
"entries": []any{},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.ParseTextUnits(context.Background(), TextUnitQueryParams{
|
|
Offset: 3,
|
|
Limit: 5,
|
|
Destination: "*Table*",
|
|
PathID: &pathID,
|
|
ClassID: &classID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ParseTextUnits error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestUnityFSPatchFieldSendsTaggedReplacement(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "unityfs.patch_field" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params UnityFSFieldPatchParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.BundlePath != "/tmp/source.bundle" || params.FieldPath != "m_Name" || string(params.Replacement) != `{"kind":"string","value":"new text"}` {
|
|
t.Fatalf("params = %#v replacement=%s", params, string(params.Replacement))
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-unityfs",
|
|
Data: map[string]any{
|
|
"command": "unityfs.patch_field",
|
|
"status": "completed",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.UnityFSPatchField(context.Background(), UnityFSFieldPatchParams{
|
|
BundlePath: "/tmp/source.bundle",
|
|
SerializedFilePath: "CAB-test",
|
|
PathID: 1,
|
|
FieldPath: "m_Name",
|
|
Replacement: json.RawMessage(`{"kind":"string","value":"new text"}`),
|
|
TargetPath: "/tmp/target.bundle",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UnityFSPatchField error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestResourceListSendsPagination(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "resource.list" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params pageParam
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.Offset != 2 || params.Limit != 1 {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
bytes := uint64(42)
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-2",
|
|
Data: map[string]any{
|
|
"available": true,
|
|
"resource_root": "/tmp/resources/current",
|
|
"manifest_version": 1,
|
|
"total_entries": 3,
|
|
"offset": 2,
|
|
"limit": 1,
|
|
"entries": []map[string]any{
|
|
{
|
|
"url": "https://example.invalid/a.zip",
|
|
"destination": "a.zip",
|
|
"bytes": bytes,
|
|
"blake3": "abc",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
page, err := client.ResourceList(context.Background(), 2, 1)
|
|
if err != nil {
|
|
t.Fatalf("ResourceList error: %v", err)
|
|
}
|
|
if !page.Available || page.TotalEntries != 3 || len(page.Entries) != 1 {
|
|
t.Fatalf("unexpected page: %#v", page)
|
|
}
|
|
if page.Entries[0].Bytes == nil || *page.Entries[0].Bytes != 42 {
|
|
t.Fatalf("unexpected entry bytes: %#v", page.Entries[0].Bytes)
|
|
}
|
|
}
|
|
|
|
func TestScheduleUpdateSendsMutationParams(t *testing.T) {
|
|
every := uint64(3600)
|
|
count := uint64(4)
|
|
enabled := true
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "schedule.update" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params ScheduleMutationParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.ID != "nightly-pull" || params.Group != "res" || params.Action != "pull" ||
|
|
params.EverySeconds == nil || *params.EverySeconds != every ||
|
|
params.Count == nil || *params.Count != count || params.Enabled == nil || !*params.Enabled {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-schedule",
|
|
Data: map[string]any{
|
|
"command": "schedule-update",
|
|
"status": "updated",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.ScheduleUpdate(context.Background(), ScheduleMutationParams{
|
|
ID: "nightly-pull",
|
|
Group: "res",
|
|
Action: "pull",
|
|
EverySeconds: &every,
|
|
Count: &count,
|
|
Enabled: &enabled,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScheduleUpdate error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestScheduleRunSendsScopeAndMaxRuns(t *testing.T) {
|
|
maxRuns := uint64(2)
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "schedule.run" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params ScheduleRunParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.ID != "nightly-pull" || params.Group != "res" || !params.Force ||
|
|
params.MaxRuns == nil || *params.MaxRuns != maxRuns {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-schedule-run",
|
|
Data: map[string]any{"command": "schedule-run", "executed": []any{}},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.ScheduleRun(context.Background(), ScheduleRunParams{
|
|
ID: "nightly-pull",
|
|
Group: "res",
|
|
Force: true,
|
|
MaxRuns: &maxRuns,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScheduleRun error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestTranslationTasksSendsQueryParams(t *testing.T) {
|
|
offset := uint64(10)
|
|
limit := uint64(25)
|
|
hasReason := true
|
|
hasFailureReason := false
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "translation.tasks" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params TranslationTaskListParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.Offset == nil || *params.Offset != offset ||
|
|
params.Limit == nil || *params.Limit != limit ||
|
|
params.TaskID != "textunit/v-current/Scenario" ||
|
|
params.ReleaseID != "v-current" ||
|
|
params.Destination != "MediaResources/GameData/Scenario.zip" ||
|
|
params.PathPattern != "Scenario" ||
|
|
params.ArchiveEntry != "ScenarioExcelTable.json" ||
|
|
params.Status != "queued_offline" ||
|
|
params.WorkerStatus != "failed" ||
|
|
params.ParseStatus != "ok" ||
|
|
params.Format != "json" ||
|
|
params.HasReason == nil || *params.HasReason != hasReason ||
|
|
params.HasFailureReason == nil || *params.HasFailureReason != hasFailureReason {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-translation-tasks",
|
|
Data: map[string]any{"tasks": []any{}},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.TranslationTasks(context.Background(), TranslationTaskListParams{
|
|
Offset: &offset,
|
|
Limit: &limit,
|
|
TaskID: "textunit/v-current/Scenario",
|
|
ReleaseID: "v-current",
|
|
Destination: "MediaResources/GameData/Scenario.zip",
|
|
PathPattern: "Scenario",
|
|
ArchiveEntry: "ScenarioExcelTable.json",
|
|
Status: "queued_offline",
|
|
WorkerStatus: "failed",
|
|
ParseStatus: "ok",
|
|
Format: "json",
|
|
HasReason: &hasReason,
|
|
HasFailureReason: &hasFailureReason,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("TranslationTasks error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestTranslationHandoffUsesRustMethod(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "translation.handoff" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
if string(req.Params) != "null" {
|
|
t.Fatalf("params = %s, want null", req.Params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-translation-handoff",
|
|
Data: map[string]any{"jobs": []any{}},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.TranslationHandoff(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TranslationHandoff error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestTranslationTaskUpdateSendsWorkerParams(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "translation.task.update" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params TranslationTaskUpdateParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.TaskID != "textunit/v-current/Scenario" ||
|
|
params.Status != "completed" ||
|
|
params.FailureReason != "" ||
|
|
params.ProviderRunID != "provider-run-1" ||
|
|
params.Provider != "manual" ||
|
|
len(params.TranslationResults) != 1 ||
|
|
params.TranslationResults[0].UnitID != "direct:a#unit:0" ||
|
|
params.TranslationResults[0].SourceText != "source" ||
|
|
params.TranslationResults[0].TranslatedText != "译文" {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-translation-update",
|
|
Data: map[string]any{"task_status": "completed"},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.TranslationTaskUpdate(context.Background(), TranslationTaskUpdateParams{
|
|
TaskID: "textunit/v-current/Scenario",
|
|
Status: "completed",
|
|
ProviderRunID: "provider-run-1",
|
|
Provider: "manual",
|
|
TranslationResults: []TranslationTaskUnitResultParam{{
|
|
UnitID: "direct:a#unit:0",
|
|
SourceText: "source",
|
|
TranslatedText: "译文",
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("TranslationTaskUpdate error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
|
var concurrency uint64 = 8
|
|
var maxAttempts uint64 = 4
|
|
var leaseSeconds uint64 = 60
|
|
var retryBackoff uint64 = 0
|
|
var maxTasks uint64 = 2
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "translation.worker.run" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params TranslationWorkerRunParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.Provider != "mock" ||
|
|
params.FixturePath != "/tmp/mock-provider.json" ||
|
|
params.TranslationMemoryPath != "/tmp/translation-memory.sqlite" ||
|
|
params.Concurrency == nil || *params.Concurrency != concurrency ||
|
|
params.MaxAttempts == nil || *params.MaxAttempts != maxAttempts ||
|
|
params.LeaseSeconds == nil || *params.LeaseSeconds != leaseSeconds ||
|
|
params.RetryBackoffSeconds == nil || *params.RetryBackoffSeconds != retryBackoff ||
|
|
params.MaxTasks == nil || *params.MaxTasks != maxTasks ||
|
|
params.WorkerID != "dashboard-worker" {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "accepted",
|
|
RequestID: "req-test-translation-worker",
|
|
Data: map[string]any{
|
|
"task_id": "task-worker-1",
|
|
"kind": "translation.worker.run",
|
|
"worker": map[string]any{
|
|
"provider": "mock",
|
|
"fixture_path": "/tmp/mock-provider.json",
|
|
"translation_memory_path": "/tmp/translation-memory.sqlite",
|
|
"concurrency": concurrency,
|
|
"max_attempts": maxAttempts,
|
|
"lease_seconds": leaseSeconds,
|
|
"retry_backoff_seconds": retryBackoff,
|
|
"max_tasks": maxTasks,
|
|
"worker_id": "dashboard-worker",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
result, err := client.TranslationWorkerRun(context.Background(), TranslationWorkerRunParams{
|
|
Provider: "mock",
|
|
FixturePath: "/tmp/mock-provider.json",
|
|
TranslationMemoryPath: "/tmp/translation-memory.sqlite",
|
|
Concurrency: &concurrency,
|
|
MaxAttempts: &maxAttempts,
|
|
LeaseSeconds: &leaseSeconds,
|
|
RetryBackoffSeconds: &retryBackoff,
|
|
MaxTasks: &maxTasks,
|
|
WorkerID: "dashboard-worker",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("TranslationWorkerRun error: %v", err)
|
|
}
|
|
if result.TaskID != "task-worker-1" ||
|
|
result.Kind != "translation.worker.run" ||
|
|
result.Worker.Concurrency != concurrency ||
|
|
result.Worker.TranslationMemoryPath != "/tmp/translation-memory.sqlite" ||
|
|
result.Worker.RetryBackoffSeconds != retryBackoff ||
|
|
result.Worker.MaxTasks == nil || *result.Worker.MaxTasks != maxTasks {
|
|
t.Fatalf("unexpected result: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestTranslationMemoryTypedContract(t *testing.T) {
|
|
limit := uint64(25)
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
switch req.Method {
|
|
case "translation.memory.summary":
|
|
var params TranslationMemorySummaryParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode summary params: %v", err)
|
|
}
|
|
if params.TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" {
|
|
t.Fatalf("summary params=%#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true, Status: "ok", RequestID: "req-tm-summary",
|
|
Data: map[string]any{
|
|
"available": true,
|
|
"path": "/var/lib/bat/translation-memory.sqlite",
|
|
"schema_version": 1,
|
|
"summary": map[string]any{
|
|
"schema_version": 1,
|
|
"record_count": 3,
|
|
"trusted_count": 1,
|
|
"candidate_count": 1,
|
|
"superseded_count": 1,
|
|
"rejected_count": 0,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "translation.memory.query":
|
|
var params TranslationMemoryQueryParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode query params: %v", err)
|
|
}
|
|
if params.SourceText != "Hello" ||
|
|
params.SourceContext["destination"] != "Bundle/dialogue.bundle" ||
|
|
params.Limit == nil || *params.Limit != limit {
|
|
t.Fatalf("query params=%#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true, Status: "ok", RequestID: "req-tm-query",
|
|
Data: map[string]any{
|
|
"available": true,
|
|
"path": "/var/lib/bat/translation-memory.sqlite",
|
|
"source_text": "Hello",
|
|
"source_context": map[string]any{
|
|
"destination": "Bundle/dialogue.bundle",
|
|
"field_path": "Dialog.Message",
|
|
},
|
|
"matches": []any{
|
|
map[string]any{
|
|
"entry": map[string]any{
|
|
"record_id": "tm-record-1",
|
|
"source_text": "Hello",
|
|
"source_hash": "hash-source",
|
|
"normalized_source_text": "hello",
|
|
"source_context": map[string]any{
|
|
"destination": "Bundle/dialogue.bundle",
|
|
"field_path": "Dialog.Message",
|
|
},
|
|
"source_context_hash": "hash-context",
|
|
"translated_text": "你好",
|
|
"translation_source_kind": "provider",
|
|
"trust_status": "trusted",
|
|
"official_release_id": "release-1",
|
|
"source_trace": map[string]any{
|
|
"official_release_id": "release-1",
|
|
"unit_id": "textunit-1",
|
|
"task_id": "task-1",
|
|
"destination": "Bundle/dialogue.bundle",
|
|
"archive_entry": "dialogue.json",
|
|
"serialized_file": "globalgamemanagers",
|
|
"path_id": 42,
|
|
"class_id": 114,
|
|
"field_path": "Dialog.Message",
|
|
"format": "json",
|
|
"asset_name": "Dialogue",
|
|
"text_source_kind": "text_asset",
|
|
},
|
|
"provider": "mock",
|
|
"provider_run_id": "provider-run-1",
|
|
"created_unix_seconds": 100,
|
|
"updated_unix_seconds": 200,
|
|
"trusted_unix_seconds": 200,
|
|
"trusted_by": "reviewer",
|
|
"trusted_reason": "reviewed",
|
|
},
|
|
"match_kind": "strong_exact",
|
|
"can_auto_reuse": true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "translation.memory.confirm":
|
|
var params TranslationMemoryConfirmParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode confirm params: %v", err)
|
|
}
|
|
if params.RecordID != "tm-record-1" ||
|
|
params.Reviewer != "reviewer" ||
|
|
params.Reason != "reviewed" ||
|
|
params.TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" {
|
|
t.Fatalf("confirm params=%#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true, Status: "ok", RequestID: "req-tm-confirm",
|
|
Data: map[string]any{
|
|
"available": true,
|
|
"path": "/var/lib/bat/translation-memory.sqlite",
|
|
"entry": map[string]any{
|
|
"record_id": "tm-record-1",
|
|
"translation_source_kind": "provider",
|
|
"trust_status": "trusted",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected method %q", req.Method)
|
|
return testResponse{}
|
|
}
|
|
})
|
|
|
|
summary, err := client.TranslationMemorySummary(context.Background(), TranslationMemorySummaryParams{
|
|
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("TranslationMemorySummary error: %v", err)
|
|
}
|
|
if !summary.Available || summary.Summary == nil ||
|
|
summary.Summary.TrustedCount != 1 || summary.Summary.CandidateCount != 1 {
|
|
t.Fatalf("summary=%#v", summary)
|
|
}
|
|
|
|
query, err := client.TranslationMemoryQuery(context.Background(), TranslationMemoryQueryParams{
|
|
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
|
SourceText: "Hello",
|
|
SourceContext: TranslationMemoryContext{
|
|
"destination": "Bundle/dialogue.bundle",
|
|
"field_path": "Dialog.Message",
|
|
},
|
|
Limit: &limit,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("TranslationMemoryQuery error: %v", err)
|
|
}
|
|
if len(query.Matches) != 1 ||
|
|
query.Matches[0].MatchKind != "strong_exact" ||
|
|
!query.Matches[0].CanAutoReuse ||
|
|
query.Matches[0].Entry.TrustStatus != "trusted" ||
|
|
query.Matches[0].Entry.TranslationSourceKind != "provider" ||
|
|
query.Matches[0].Entry.SourceTrace.UnitID == nil ||
|
|
*query.Matches[0].Entry.SourceTrace.UnitID != "textunit-1" ||
|
|
query.Matches[0].Entry.SourceTrace.PathID == nil ||
|
|
*query.Matches[0].Entry.SourceTrace.PathID != 42 ||
|
|
query.Matches[0].Entry.TrustedBy == nil ||
|
|
*query.Matches[0].Entry.TrustedBy != "reviewer" {
|
|
t.Fatalf("query=%#v", query)
|
|
}
|
|
|
|
confirmed, err := client.TranslationMemoryConfirm(context.Background(), TranslationMemoryConfirmParams{
|
|
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
|
RecordID: "tm-record-1",
|
|
Reviewer: "reviewer",
|
|
Reason: "reviewed",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("TranslationMemoryConfirm error: %v", err)
|
|
}
|
|
if !confirmed.Available || confirmed.Entry.TrustStatus != "trusted" ||
|
|
confirmed.Entry.RecordID != "tm-record-1" {
|
|
t.Fatalf("confirmed=%#v", confirmed)
|
|
}
|
|
}
|
|
|
|
func TestTranslationProofreadUsesRustMethod(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "translation.proofread" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
if string(req.Params) != "null" {
|
|
t.Fatalf("params = %s, want null", req.Params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-translation-proofread",
|
|
Data: map[string]any{
|
|
"translation_workflow_status": "manual_proofreading",
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.TranslationProofread(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TranslationProofread error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestLocalizedPublishSendsWorkerSourceAndReleaseOptions(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "localized.publish" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params LocalizedPublishParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if !params.FromWorker || params.TranslationFile != "" ||
|
|
params.LocalizedReleaseID != "localized-1" || !params.Force {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-localized-publish",
|
|
Data: map[string]any{"status": "published"},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.LocalizedPublish(context.Background(), LocalizedPublishParams{
|
|
FromWorker: true,
|
|
LocalizedReleaseID: "localized-1",
|
|
Force: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LocalizedPublish error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestLocalizedRollbackSendsExpectedRelease(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "localized.rollback" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
var params LocalizedRollbackParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
t.Fatalf("decode params: %v", err)
|
|
}
|
|
if params.LocalizedReleaseID != "localized-1" {
|
|
t.Fatalf("params = %#v", params)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: true,
|
|
Status: "ok",
|
|
RequestID: "req-test-localized-rollback",
|
|
Data: map[string]any{"status": "rolled_back"},
|
|
},
|
|
}
|
|
})
|
|
|
|
raw, err := client.LocalizedRollback(context.Background(), LocalizedRollbackParams{
|
|
LocalizedReleaseID: "localized-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LocalizedRollback error: %v", err)
|
|
}
|
|
if !json.Valid(raw) {
|
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
|
}
|
|
}
|
|
|
|
func TestApplicationErrorReturnsAPIError(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
if req.Method != "task.status" {
|
|
t.Fatalf("method = %s", req.Method)
|
|
}
|
|
return testResponse{
|
|
Result: testEnvelope{
|
|
OK: false,
|
|
Status: "error",
|
|
Error: APIError{
|
|
Code: "BAT-ERR-700004",
|
|
Kind: "task_not_found",
|
|
Domain: "rpc",
|
|
Location: "task.status",
|
|
Message: "任务不存在:missing",
|
|
Retryable: false,
|
|
},
|
|
RequestID: "req-test-3",
|
|
},
|
|
}
|
|
})
|
|
|
|
_, err := client.TaskStatus(context.Background(), "missing")
|
|
var apiErr *APIError
|
|
if !errors.As(err, &apiErr) {
|
|
t.Fatalf("expected APIError, got %T %v", err, err)
|
|
}
|
|
if apiErr.Code != "BAT-ERR-700004" {
|
|
t.Fatalf("code = %s", apiErr.Code)
|
|
}
|
|
}
|
|
|
|
func TestTransportErrorReturnsJSONRPCError(t *testing.T) {
|
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
|
return testResponse{
|
|
Error: &JSONRPCError{
|
|
Code: -32700,
|
|
Message: "parse error",
|
|
},
|
|
}
|
|
})
|
|
|
|
_, err := client.DaemonStatus(context.Background())
|
|
var rpcErr *JSONRPCError
|
|
if !errors.As(err, &rpcErr) {
|
|
t.Fatalf("expected JSONRPCError, got %T %v", err, err)
|
|
}
|
|
if rpcErr.Code != -32700 {
|
|
t.Fatalf("code = %d", rpcErr.Code)
|
|
}
|
|
}
|
|
|
|
func TestContextDeadlineIsApplied(t *testing.T) {
|
|
client := New("bat.sock")
|
|
client.Timeout = time.Millisecond
|
|
client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := client.DaemonStatus(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected dial error")
|
|
}
|
|
}
|