2026-08-10 13:59:33 +09:00
|
|
|
import { Worker } from "node:worker_threads";
|
|
|
|
|
import { performance } from "node:perf_hooks";
|
|
|
|
|
import { generateMap } from "../src/mapPipeline.js";
|
|
|
|
|
import { createWorldMap } from "../src/worldMap.js";
|
2026-08-11 21:51:07 +09:00
|
|
|
import { buildMaximumProductionLasso, derivePatchSeed } from "./production-fixtures.mjs";
|
2026-08-10 13:59:33 +09:00
|
|
|
|
|
|
|
|
function assert(condition, message) {
|
|
|
|
|
if (!condition) throw new Error(message);
|
|
|
|
|
console.log(`OK: ${message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initial = generateMap(114514);
|
|
|
|
|
const world = createWorldMap(initial);
|
2026-08-11 21:51:07 +09:00
|
|
|
const rect = buildMaximumProductionLasso(world);
|
2026-08-10 13:59:33 +09:00
|
|
|
const terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
|
2026-08-11 21:51:07 +09:00
|
|
|
const seed = derivePatchSeed(world.seed, terrainType, 0);
|
2026-08-10 13:59:33 +09:00
|
|
|
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
|
|
|
|
|
let terminalMessage = null;
|
|
|
|
|
let cancelStartedAt = 0;
|
|
|
|
|
let terminationMs = Infinity;
|
|
|
|
|
let cancellationTriggered = false;
|
|
|
|
|
|
|
|
|
|
const timeout = setTimeout(async () => {
|
|
|
|
|
try { await worker.terminate(); } catch {}
|
|
|
|
|
console.error("FAIL: production patch worker did not reach cancellable large-precompute work in time");
|
|
|
|
|
process.exit(2);
|
|
|
|
|
}, 35_000);
|
|
|
|
|
|
|
|
|
|
worker.on("message", async (message) => {
|
|
|
|
|
if (message.id !== 1) return;
|
|
|
|
|
if (message.type !== "progress") {
|
|
|
|
|
terminalMessage = message;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const progress = message.progress || {};
|
|
|
|
|
if (cancellationTriggered || progress.phase !== "large-candidate-precompute") return;
|
|
|
|
|
cancellationTriggered = true;
|
|
|
|
|
// Let the coordinator enter nested-worker work rather than measuring an idle
|
|
|
|
|
// worker immediately after the phase marker.
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
|
cancelStartedAt = performance.now();
|
|
|
|
|
await worker.terminate();
|
|
|
|
|
terminationMs = performance.now() - cancelStartedAt;
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
assert(terminationMs < 500, `production patch Worker termination settles within 500 ms (${terminationMs.toFixed(1)} ms)`);
|
|
|
|
|
// terminate() is the hard cancellation boundary. No terminal candidate can
|
|
|
|
|
// be published after it resolves.
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
|
|
|
assert(terminalMessage === null, "terminated production Worker cannot publish a candidate after cancellation");
|
|
|
|
|
console.log("All patch-worker cancellation tests passed.");
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
worker.on("error", async (error) => {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
try { await worker.terminate(); } catch {}
|
|
|
|
|
console.error(error?.stack || String(error));
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
worker.postMessage({
|
|
|
|
|
id: 1,
|
|
|
|
|
world,
|
|
|
|
|
rect,
|
|
|
|
|
options: {
|
|
|
|
|
patchMode: "expansion",
|
|
|
|
|
terrainType,
|
|
|
|
|
variant: 0,
|
|
|
|
|
seed,
|
|
|
|
|
maxQualityRetries: 0,
|
|
|
|
|
qualityTerrainAttempts: 1,
|
|
|
|
|
acceptBestAvailableQuality: false,
|
|
|
|
|
includeSeamVisualization: false,
|
|
|
|
|
},
|
|
|
|
|
search: {
|
|
|
|
|
searchId: "cancel-integration",
|
|
|
|
|
operationId: "cancel-integration",
|
|
|
|
|
committedRevision: 1,
|
|
|
|
|
workerEpoch: 1,
|
|
|
|
|
executionAttempt: 1,
|
|
|
|
|
totalCandidateCount: 1,
|
|
|
|
|
candidatePlan: [{ candidateId: "cancel:0", candidateOrdinal: 1, variant: 0, seed }],
|
|
|
|
|
},
|
|
|
|
|
});
|