Refresh the homepage and improve export and draft guidance

This commit is contained in:
2026-10-04 17:48:39 +05:00
parent 04cc4f39f6
commit 244d27a309
33 changed files with 2110 additions and 215 deletions

232
scripts/test-plugin.ts Normal file
View File

@@ -0,0 +1,232 @@
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<void>((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<typeof setTimeout>;
}[] = [];
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<void>((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<void>((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<void>((r) => api.close(() => r()));
await writeFile(`${directory}/regression.log`, output);
}