Files
pgames-manager/frontend/tests/translator-run.test.mjs
T
mrleo1nidandClaude Sonnet 5.5 e8d6bf4646
CI / frontend (push) Failing after 17s
CI / backend (push) Successful in 47s
feat: machine translation through a local translator running in the browser's machine
Translator panel in the translation editor and a /translator setup page. The browser talks to
local-translator directly; the server only receives lines marked as machine.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 18:59:27 +03:00

141 lines
6.3 KiB
JavaScript

import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { build } from "esbuild";
const load = async (file) => {
const out = await build({ entryPoints: [fileURLToPath(new URL(`../src/translator/${file}`, import.meta.url))],
bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" });
return import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString("base64")}`);
};
const { runTranslation } = await load("run.ts");
const { TranslatorClient, normalizeUrl } = await load("client.ts");
// A translator that finishes jobs at once; `broken` lines come back failed.
function fakeTranslator({ broken = [] } = {}) {
const jobs = new Map();
const created = [];
return {
created, jobs,
async createJob(_project, items) {
const job = { id: `j${jobs.size}`, status: "completed_with_errors", model: "m", error: null,
items: items.map((it) => broken.includes(it.text)
? { id: it.id, source: it.text, translation: null, status: "failed", error: "bad" }
: { id: it.id, source: it.text, translation: `RU ${it.text}`, status: "completed", error: null }) };
jobs.set(job.id, job);
created.push(items);
return job;
},
async job(id) { return jobs.get(id); },
async cancel(id) { return { ...jobs.get(id), status: "cancelled" }; },
};
}
// The "server": untranslated lines match the filter until they are saved.
function fakeServer(texts) {
const rows = texts.map((source, i) => ({ key: `k${i}`, source }));
const saved = {};
return {
saved,
fetchPage: async (skip, limit) => {
const open = rows.filter((r) => !(r.key in saved));
return { total: open.length, items: open.slice(skip, skip + limit) };
},
save: async (results) => { for (const r of results) saved[r.key] = r.text; },
};
}
const run = (client, server, extra = {}) => runTranslation({
client, projectId: "p", batchSize: 10, pageSize: 2, signal: new AbortController().signal,
...server, onProgress: () => {}, pollMs: 1, ...extra,
});
test("every line is translated and saved page by page", async () => {
const server = fakeServer(["a", "b", "c", "d", "e"]);
const translator = fakeTranslator();
const seen = [];
const outcome = await run(translator, server, { onProgress: (p) => seen.push(p) });
assert.equal(outcome, "finished");
assert.equal(translator.created.length, 3);
assert.deepEqual(server.saved, { k0: "RU a", k1: "RU b", k2: "RU c", k3: "RU d", k4: "RU e" });
assert.deepEqual([seen.at(-1).done, seen.at(-1).failed, seen.at(-1).total], [5, 0, 5]);
});
test("a line that fails is skipped and the run still ends", async () => {
const server = fakeServer(["a", "bad", "c", "d"]);
const seen = [];
await run(fakeTranslator({ broken: ["bad"] }), server, { onProgress: (p) => seen.push(p) });
assert.deepEqual(Object.keys(server.saved), ["k0", "k2", "k3"]);
assert.deepEqual([seen.at(-1).done, seen.at(-1).failed, seen.at(-1).lastError], [3, 1, "bad"]);
});
test("empty lines are not sent to the model and do not loop forever", async () => {
const translator = fakeTranslator();
await run(translator, fakeServer(["a", " ", "", "b"]), { pageSize: 10 });
assert.deepEqual(translator.created.flat().map((i) => i.text), ["a", "b"]);
});
test("stopping cancels the job and saves nothing more", async () => {
const server = fakeServer(["a", "b", "c"]);
const translator = fakeTranslator();
const ctl = new AbortController();
const client = {
...translator,
async createJob(...args) {
const job = await translator.createJob(...args);
job.status = "running";
job.items.forEach((i) => { i.status = "pending"; i.translation = null; });
ctl.abort();
return job;
},
};
const outcome = await run(client, server, { pageSize: 3, signal: ctl.signal });
assert.equal(outcome, "stopped");
assert.deepEqual(server.saved, {});
});
test("an unreachable translator ends the run with an error", async () => {
const translator = fakeTranslator();
const dead = {
...translator,
async createJob(...args) { const j = await translator.createJob(...args); j.status = "running"; return j; },
async job() { throw new Error("offline"); },
};
await assert.rejects(run(dead, fakeServer(["a"]), { pageSize: 3 }), /offline/);
});
test("the client tells a stopped translator from one that blocks this site", async () => {
const health = { status: "ok", api_version: 2, worker_running: true, startup_errors: {} };
const ok = new TranslatorClient("http://x", async () => new Response(JSON.stringify(health)));
assert.equal((await ok.diagnose()).state, "ok");
const old = new TranslatorClient("http://x", async () => new Response(JSON.stringify({ ...health, api_version: undefined })));
assert.equal((await old.diagnose()).state, "old");
// CORS failure: the normal request throws, the opaque no-cors one succeeds
const blocked = new TranslatorClient("http://x", async (_u, init) => {
if (init?.mode === "no-cors") return new Response("");
throw new TypeError("Failed to fetch");
});
assert.equal((await blocked.diagnose()).state, "blocked");
const offline = new TranslatorClient("http://x", async () => { throw new TypeError("Failed to fetch"); });
assert.equal((await offline.diagnose()).state, "offline");
});
test("a project is updated when it exists and created when it does not", async () => {
const calls = [];
const fetcher = async (url, init) => {
calls.push(`${init.method} ${url}`);
if (init.method === "GET") return new Response(JSON.stringify([{ id: "p1", name: "Game" }]));
return new Response(JSON.stringify({ id: "new" }));
};
const client = new TranslatorClient("http://x", fetcher);
const s = { source_language: "English", target_language: "Russian", context: "", instructions: "", glossary: {}, provider: "lmstudio", model: null };
assert.equal(await client.saveProject({ ...s, name: "Game" }), "p1");
assert.equal(await client.saveProject({ ...s, name: "Other" }), "new");
assert.deepEqual(calls, ["GET http://x/projects", "PUT http://x/projects/p1", "GET http://x/projects", "POST http://x/projects"]);
});
test("addresses are completed and cleaned", () => {
assert.equal(normalizeUrl(" 127.0.0.1:8787/ "), "http://127.0.0.1:8787");
assert.equal(normalizeUrl("https://t.example//"), "https://t.example");
});