import { createWorldMap } from "../src/worldMap.js"; const params = new URLSearchParams(location.search); const resultEl = document.getElementById("result"); const seed = (Number(params.get("seed")) || 114514) >>> 0; const startVariant = (Number(params.get("variant")) || 0) >>> 0; const candidateLimit = Math.max(1, Math.min(3, Number(params.get("candidates")) || 2)); const selectionWidth = Math.max(48, Math.floor(Number(params.get("width")) || 60)); const selectionHeight = Math.max(48, Math.floor(Number(params.get("height")) || 60)); const patchBudgetMs = Math.max(1000, Number(params.get("budgetMs")) || 60000); function deriveSeed(worldSeed, terrainType, variant) { let h = (worldSeed >>> 0) ^ 0x9e3779b9; h = Math.imul(h ^ (variant >>> 0), 668265263) >>> 0; for (const ch of String(terrainType || "auto")) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0; return h >>> 0; } function waitForGeneration(worker) { return new Promise((resolve, reject) => { const id = 1; const progress = []; const onMessage = (event) => { if (event.data?.id !== id) return; if (event.data.type === "progress") { progress.push({ at: performance.now(), ...(event.data.progress || event.data.event || {}) }); return; } worker.removeEventListener("message", onMessage); if (event.data.ok) resolve({ map: event.data.map, progress }); else reject(new Error(event.data.error || "Initial generation failed")); }; worker.addEventListener("message", onMessage); worker.addEventListener("error", (event) => reject(new Error(event.message || "Initial generation Worker crashed")), { once: true }); worker.postMessage({ id, seed, options: { terrainType: params.get("terrain") || "auto" } }); }); } function waitForApplyAck(worker, patch) { return new Promise((resolve, reject) => { const applyToken = patch.result?.applyToken; if (!applyToken) { reject(new Error("Accepted patch did not provide a transactional Apply token.")); return; } const ackId = `e2e-apply-${Date.now()}`; const timer = setTimeout(() => reject(new Error("Transactional Apply ACK timed out.")), 30000); const onMessage = (event) => { const data = event.data || {}; if (data.type !== "patch-apply-ack-result" || data.ackId !== ackId) return; clearTimeout(timer); worker.removeEventListener("message", onMessage); if (data.ok) resolve(data); else reject(new Error(data.error || "Transactional Apply ACK failed.")); }; worker.addEventListener("message", onMessage); worker.postMessage({ type: "patch-apply-ack", ackId, applyToken, baseCommittedRevision: 1, committedRevision: 2, }); }); } function heapSnapshot(label) { return performance.memory ? { label, usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize, jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, } : null; } function waitForPatch(worker, message) { return new Promise((resolve, reject) => { const progress = []; const startedAt = performance.now(); const onMessage = (event) => { if (event.data?.id !== message.id) return; if (event.data.type === "progress") { progress.push({ at: performance.now(), ...event.data.progress }); return; } worker.removeEventListener("message", onMessage); if (event.data.ok) resolve({ ...event.data, progress, wallMs: performance.now() - startedAt }); else reject(new Error(event.data.error || "Patch Worker failed")); }; worker.addEventListener("message", onMessage); worker.addEventListener("messageerror", () => reject(new Error("Patch result could not be deserialized")), { once: true }); worker.addEventListener("error", (event) => reject(new Error(event.message || "Patch Worker crashed")), { once: true }); worker.postMessage(message); }); } function maxProgressGap(progress, start, end) { const times = [start, ...(progress || []).map((entry) => entry.at), end]; let max = 0; for (let index = 1; index < times.length; index++) max = Math.max(max, times[index] - times[index - 1]); return max; } async function main() { const heap = [heapSnapshot("start")].filter(Boolean); const initialWorker = new Worker(new URL("../src/generationWorker.js", import.meta.url), { type: "module" }); const initialStartedAt = performance.now(); const initial = await waitForGeneration(initialWorker); const afterInitialHeap = heapSnapshot("after-initial"); if (afterInitialHeap) heap.push(afterInitialHeap); const initialEndedAt = performance.now(); initialWorker.terminate(); const world = createWorldMap(initial.map); const rect = { x0: world.originX + Math.max(0, Math.floor((258 - selectionWidth) / 2)), y0: world.originY + Math.max(0, Math.floor((183 - selectionHeight) / 2)), x1: world.originX + Math.max(0, Math.floor((258 - selectionWidth) / 2)) + selectionWidth, y1: world.originY + Math.max(0, Math.floor((183 - selectionHeight) / 2)) + selectionHeight, }; if (params.get("shape") === "lasso") { const insetX = Math.max(4, Math.floor(selectionWidth * 0.16)); const insetY = Math.max(4, Math.floor(selectionHeight * 0.16)); rect.kind = "lasso"; rect.polygon = [ { x: rect.x0 + insetX, y: rect.y0 }, { x: rect.x1 - 1, y: rect.y0 + insetY }, { x: rect.x1 - insetX, y: rect.y1 - 1 }, { x: rect.x0, y: rect.y1 - insetY }, ]; } const terrainType = params.get("patchTerrain") || initial.map.terrainTemplate?.terrainType || "auto"; const candidatePlan = Array.from({ length: candidateLimit }, (_, index) => { const variant = (startVariant + index) >>> 0; return { candidateId: `e2e:${variant}`, candidateOrdinal: index + 1, variant, seed: deriveSeed(world.seed, terrainType, variant) }; }); const patchWorker = new Worker(new URL("../src/mapPatchWorker.js", import.meta.url), { type: "module" }); const patchStartedAt = performance.now(); const patch = await waitForPatch(patchWorker, { id: 2, world, rect, options: { patchMode: params.get("mode") || "regeneration", terrainType, variant: startVariant, seed: candidatePlan[0].seed, maxQualityRetries: 0, qualityTerrainAttempts: 1, acceptBestAvailableQuality: false, includeSeamVisualization: false, }, search: { searchId: "browser-e2e", operationId: "browser-e2e", committedRevision: 1, workerEpoch: 1, candidatePlan, totalCandidateCount: candidateLimit, }, }); const patchEndedAt = performance.now(); const afterPatchHeap = heapSnapshot("after-patch"); if (afterPatchHeap) heap.push(afterPatchHeap); if (patch.result?.ok !== true) { throw new Error(`No accepted preview was produced (${patch.result?.code || patch.result?.searchStatus || "unknown rejection"}).`); } const applyAck = await waitForApplyAck(patchWorker, patch); const afterApplyHeap = heapSnapshot("after-apply-ack"); if (afterApplyHeap) heap.push(afterApplyHeap); patchWorker.terminate(); const attempts = patch.result?.searchAttempts || []; const boundedEvents = patch.progress.filter((entry) => entry.boundedWork === true); const invalidBoundedEvents = boundedEvents.filter((entry) => !Number.isFinite(entry.completed) || !Number.isFinite(entry.total) || entry.completed < 0 || entry.total < 0 || entry.completed > entry.total); const assertions = { workerTransportSucceeded: patch.ok === true, candidateAuditPresent: attempts.length > 0, previewPublished: patch.result?.ok === true && patch.result?.searchStatus === "succeeded", boundedAttempts: attempts.length <= candidateLimit, fullPipelineTimingsPresent: attempts.every((attempt) => (attempt.patchTimings || []).some((entry) => entry.key === "candidate" || entry.key === "tiled-total" || entry.key === "tiled-regeneration-total")), noBestAvailableAcceptance: attempts.every((attempt) => attempt.candidateQuality?.acceptedAsBestAvailable !== true), boundedProgressValid: boundedEvents.length > 0 && invalidBoundedEvents.length === 0, applyAckHashMatches: applyAck.mirrorHash === patch.result?.acceptedWorldHash, patchBudgetMet: patch.wallMs < patchBudgetMs, }; const report = { status: Object.values(assertions).every(Boolean) ? "pass" : "fail", environment: { userAgent: navigator.userAgent, hardwareConcurrency: navigator.hardwareConcurrency || null, deviceMemoryGiB: navigator.deviceMemory || null, crossOriginIsolated, }, workload: { seed, startVariant, candidateLimit, selection: rect, selectionWidth, selectionHeight, selectionShape: rect.kind || "rect", terrainType, patchMode: params.get("mode") || "regeneration", plannedTileUpperBound: Math.ceil(selectionWidth / Math.floor(258 / 1.72)) * Math.ceil(selectionHeight / Math.floor(183 / 1.72)), }, timing: { initialWallMs: initialEndedAt - initialStartedAt, patchWallMs: patch.wallMs, patchBudgetMs, initialMaxProgressGapMs: maxProgressGap(initial.progress, initialStartedAt, initialEndedAt), patchMaxProgressGapMs: maxProgressGap(patch.progress, patchStartedAt, patchEndedAt), }, memory: heap.length ? { snapshots: heap, peakUsedJSHeapSize: Math.max(...heap.map((entry) => entry.usedJSHeapSize)), } : null, assertions, result: { ok: patch.result?.ok === true, code: patch.result?.code || null, searchStatus: patch.result?.searchStatus || null, actualVariant: patch.result?.actualVariant ?? null, nextVariant: patch.result?.nextVariant ?? null, attempts, acceptedWorldHash: patch.result?.acceptedWorldHash || null, applyAck, }, }; document.documentElement.dataset.status = report.status; resultEl.textContent = JSON.stringify(report, null, 2); } try { await main(); } catch (error) { document.documentElement.dataset.status = "fail"; resultEl.textContent = JSON.stringify({ status: "fail", infrastructureError: error?.message || String(error), stack: error?.stack || null }, null, 2); }