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; let expectedServer: string | null = "play.regression.example.com"; 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++; stage = "uploading"; manifest = JSON.parse(bytes.toString()).manifest; json({ datasetId: "fixture" }); } else if ( req.url === "/api/v1/contributions/00000000-0000-4000-8000-000000000024" ) json({ remaining: 19, playerRemaining: 12, serverAddress: expectedServer, bounds: { minX: -32, minZ: -32, maxX: 32, maxZ: 32 }, }); else if ( req.url === "/api/v1/contributions/00000000-0000-4000-8000-000000000024/server" ) json({ serverAddress: expectedServer }); 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'\nserver-address: 'play.regression.example.com'\nchunks-per-tick: 2\nviewpoints: 24\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:/); let 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, 24); assert.equal(local.serverAddress, "play.regression.example.com"); 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); received = Buffer.alloc(0); expectedServer = "wrong.example.com"; await command( "minegeo contribute 00000000-0000-4000-8000-000000000024", /Wrong capture server/, ); assert.equal(creates, 1, "Mismatch must stop before creating a draft"); expectedServer = "PLAY.REGRESSION.EXAMPLE.COM.:25565"; await command( "minegeo contribute 00000000-0000-4000-8000-000000000024", /Upload complete\. Open/, ); assert.equal(manifest.contributionTo, "00000000-0000-4000-8000-000000000024"); assert.equal(manifest.viewpoints.length, 12); assert.equal(creates, 2); expectedServer = "wrong.example.com"; await command( "minegeo contribute 00000000-0000-4000-8000-000000000024", /Wrong capture server/, ); assert.equal(creates, 2, "A cached selection must recheck the server"); expectedServer = null; await command( "minegeo contribute 00000000-0000-4000-8000-000000000024", /Upload complete\. Open/, ); assert.equal(creates, 2, "Cleared server permits an idempotent retry"); assert.equal(finalizes, 2); await command( "minegeo contribute 00000000-0000-4000-8000-000000000024", /Upload complete\. Open/, ); assert.equal(creates, 2); await command("minegeo export Random Positions", /Export complete:/); relative = ( await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8") ).trim(); const repeated = JSON.parse( await readFile( `${directory}/plugins/MineGeo/${relative}/manifest.json`, "utf8", ), ); assert.equal(repeated.viewpoints.length, 24); assert.notDeepEqual(repeated.viewpoints, local.viewpoints); 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", "24 random positions change on the next export", "contribution command caps at 12 and retries idempotently", "wrong server blocked before upload, including cached retry", "owner override/default port and cleared address supported", ], 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); }