import { createServer } from "node:http"; import { spawn } from "node:child_process"; import { mkdir, copyFile, writeFile, readFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { gunzipSync } from "node:zlib"; import assert from "node:assert/strict"; import { datasetSchema } from "../shared/protocol"; // Disposable, loopback-only Paper test. No game client is launched. const directory = `.cache/plugin-regression-${Date.now()}`; await mkdir(`${directory}/plugins/MineGeo`, { recursive: true }); await copyFile(".cache/paper-proof/paper.jar", `${directory}/paper.jar`); await copyFile( "public/tools/plugin/minegeo-plugin-1.21.11.jar", `${directory}/plugins/minegeo.jar`, ); await writeFile(`${directory}/eula.txt`, "eula=true\n"); await writeFile( `${directory}/server.properties`, 'server-ip=127.0.0.1\nserver-port=25580\nonline-mode=true\nlevel-type=minecraft:flat\ngenerator-settings={"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:dirt","height":2},{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}\ngenerate-structures=false\nview-distance=2\nsimulation-distance=2\nspawn-protection=0\nmax-players=1\n', ); let manifest: any, received = Buffer.alloc(0), stage = "uploading", interrupted = false, creates = 0, finalizes = 0, heads = 0; const api = createServer(async (req, res) => { const chunks: Buffer[] = []; for await (const chunk of req) chunks.push(chunk); const bytes = Buffer.concat(chunks); const json = (data: unknown, status = 200) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); }; const origin = `http://127.0.0.1:${(api.address() as any).port}`; if (req.url === "/api/v1/uploads") { creates++; manifest = JSON.parse(bytes.toString()).manifest; json({ datasetId: "fixture" }); } else if (req.url === "/api/v1/datasets/fixture") json({ state: stage }); else if (req.url === "/api/v1/uploads/sign") json({ assets: manifest.assets.map((a: any) => ({ ...a, endpoint: origin + "/tus", token: "fixture-signature", objectName: a.path, })), }); else if (req.url === "/tus") { res.writeHead(201, { Location: origin + "/tus/file" }); res.end(); } else if (req.url === "/tus/file" && req.method === "HEAD") { heads++; res.writeHead(200, { "Upload-Offset": String(received.length) }); res.end(); } else if (req.url === "/tus/file" && req.method === "PATCH") { assert.equal(Number(req.headers["upload-offset"]), received.length); if (!interrupted) { interrupted = true; received = Buffer.concat([ received, bytes.subarray(0, Math.floor(bytes.length / 2)), ]); res.writeHead(503); res.end("Simulated interruption"); } else { received = Buffer.concat([received, bytes]); res.writeHead(204, { "Upload-Offset": String(received.length) }); res.end(); } } else if (req.url === "/api/v1/uploads/finalize") { finalizes++; assert.equal( createHash("sha256").update(received).digest("hex"), manifest.assets[0].sha256, ); stage = "processing"; json({ state: stage }); } else json({ error: "Unexpected request " + req.method + " " + req.url }, 404); }); await new Promise((r) => api.listen(0, "127.0.0.1", r)); await writeFile( `${directory}/plugins/MineGeo/config.yml`, `website: 'http://127.0.0.1:${(api.address() as any).port}'\ntoken: 'fixture-token'\nserver-name: 'Regression world'\nchunks-per-tick: 2\nviewpoints: 5\nmax-area-size: 2048\n`, ); const child = spawn("java", ["-Xmx1536M", "-jar", "paper.jar", "--nogui"], { cwd: directory, stdio: ["pipe", "pipe", "pipe"], windowsHide: true, }); let output = "", waiters: { pattern: RegExp; resolve: () => void; reject: (e: Error) => void; start: number; timer: ReturnType; }[] = []; const log = (bytes: Buffer) => { const text = bytes.toString(); output += text; for (const line of text.split("\n")) if (/MineGeo|Done \(|ERROR/.test(line)) process.stdout.write(line + "\n"); for (const waiter of [...waiters]) if (waiter.pattern.test(output.slice(waiter.start))) { clearTimeout(waiter.timer); waiters = waiters.filter((w) => w !== waiter); waiter.resolve(); } }; child.stdout.on("data", log); child.stderr.on("data", log); const exited = new Promise((resolve) => child.on("exit", () => { for (const w of waiters) { clearTimeout(w.timer); w.reject(new Error("Paper stopped before " + w.pattern)); } waiters = []; resolve(); }), ); const wait = (pattern: RegExp, start = output.length, timeout = 90000) => new Promise((resolve, reject) => { const w = { pattern, start, resolve, reject, timer: setTimeout(() => { waiters = waiters.filter((v) => v !== w); reject(new Error("Timed out waiting for " + pattern)); }, timeout), }; waiters.push(w); if (pattern.test(output.slice(start))) { clearTimeout(w.timer); waiters = waiters.filter((v) => v !== w); resolve(); } }); const command = async (text: string, pattern: RegExp) => { const pending = wait(pattern); child.stdin.write(text + "\n"); await pending; }; try { await wait(/Done \(/, 0, 180000); await command("minegeo upload", /No completed export/); await command( "minegeo area world 100000 100000 100032 100032", /area selected/, ); await command( "minegeo export Empty Region", /Export failed: No generated chunks/, ); await command("minegeo area world -32 -32 32 32", /area selected/); child.stdin.write("forceload add -32 -32 31 31\n"); await new Promise((r) => setTimeout(r, 5000)); await command("minegeo export Regression World", /Export complete:/); const relative = ( await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8") ).trim(); const exportDir = `${directory}/plugins/MineGeo/${relative}`; const local = datasetSchema.parse( JSON.parse(await readFile(`${exportDir}/manifest.json`, "utf8")), ); assert.equal(local.viewpoints.length, 5); const terrain = JSON.parse( gunzipSync(await readFile(`${exportDir}/terrain.json.gz`)).toString(), ); assert.ok(terrain.blocks.length > 0); assert.ok(terrain.coverage.length > 0); assert.ok( local.viewpoints.every( (p) => p.x >= -32 && p.x < 32 && p.z >= -32 && p.z < 32, ), ); await command("minegeo upload", /Upload failed: Upload paused/); await command("minegeo upload", /Upload complete\. Open/); assert.equal(creates, 1); assert.equal(finalizes, 1); assert.equal(heads, 1); await command("minegeo upload", /Upload complete\. Open/); assert.equal(finalizes, 1); await command("minegeo area world -256 -256 256 256", /area selected/); const cancelled = wait(/Cancelled\. Completed exports are kept/); child.stdin.write("minegeo export Cancel Test\nminegeo cancel\n"); await cancelled; assert.equal( ( await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8") ).trim(), relative, ); await writeFile( "artifacts-plugin-regression.json", JSON.stringify( { passed: true, checks: [ "missing export guidance", "empty area failure", "real Paper export without a player", "shared schema validation", "TUS interruption and byte-perfect resume", "repeated upload is idempotent", "cancellation preserves completed export", ], blocks: terrain.blocks.length, chunks: terrain.coverage.length, viewpoints: local.viewpoints.length, directory, }, null, 2, ), ); console.log("PLUGIN REGRESSION PASSED"); } finally { child.stdin.write("stop\n"); const kill = setTimeout(() => child.kill(), 15000); await exited; clearTimeout(kill); await new Promise((r) => api.close(() => r())); await writeFile(`${directory}/regression.log`, output); }