import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const tempDir = await mkdtemp(path.join(tmpdir(), "mechanica-catalogue-runtime-")); try { const runtime = await loadRuntimeModule(); const machines = [ { id: "planetary-gearset", title: "Planetary Gearset", category: "gearboxes-drives", difficulty: "intermediate", description: "A compact epicyclic gear train with coaxial input and output members for automatic transmissions.", engineeringFacts: [ "Planetary gearsets split torque through multiple planet pinions.", { title: "Power density", body: "The ring gear, sun gear and carrier share load paths." }, ], tags: ["gears", "transmission", "torque"], relatedMachines: ["automatic-transmission", "differential"], components: [ { id: "sun-gear", name: "Sun Gear", description: "Central gear receiving input torque." }, { id: "ring-gear", name: "Ring Gear", description: "Internal gear surrounding the planets." }, { id: "carrier", name: "Planet Carrier", children: [{ id: "pinions", name: "Planet Pinions" }], }, ], labels: [{ text: "Carrier output", targetComponentId: "carrier" }], }, { id: "automatic-transmission", title: "Automatic Transmission", category: "gearboxes-drives", difficulty: "advanced", description: "Hydraulic controls and clutches select planetary gear ratios without driver-operated shift forks.", engineeringFacts: ["Uses planetary gearsets, clutch packs, hydraulic pressure and torque converters."], tags: ["gears", "transmission", "hydraulic"], relatedMachines: ["planetary-gearset", "clutch"], components: [ { id: "clutch-pack", name: "Clutch Pack" }, { id: "valve-body", name: "Valve Body" }, ], }, { id: "centrifugal-pump", title: "Centrifugal Pump", category: "pumps-fluid", difficulty: "beginner", description: "A rotating impeller adds kinetic energy to fluid before the volute converts it to pressure.", facts: [{ label: "Flow", value: "Best for continuous flow with moderate pressure rise." }], tags: ["fluid", "pump", "impeller"], relatedMachines: ["gear-pump"], components: [ { id: "impeller", name: "Impeller", description: "Curved vanes accelerate fluid radially." }, { id: "volute", name: "Volute Casing", description: "Diffuser passage recovers pressure." }, ], }, { id: "geneva-mechanism", title: "Geneva Mechanism", category: "mechanisms", difficulty: "intermediate", description: "An intermittent indexing mechanism, also called a Maltese cross, that converts continuous rotation into stepped motion.", engineeringFacts: ["Dwell periods occur while the locking disk holds the driven wheel."], tags: ["indexing", "mechanism", "motion"], relatedMachines: ["cam-follower"], components: [ { id: "drive-pin", name: "Drive Pin" }, { id: "maltese-cross", name: "Maltese Cross Wheel" }, ], }, { id: "four-stroke-engine", title: "Four-Stroke Engine", category: "engines", difficulty: "intermediate", description: "Pistons, connecting rods, crankshaft throws and poppet valves coordinate intake, compression, power and exhaust strokes.", engineeringFacts: ["Valve timing is commonly controlled by cam lobes and followers."], tags: ["engine", "combustion", "piston"], relatedMachines: ["cam-follower"], components: [ { id: "piston", name: "Piston" }, { id: "crankshaft", name: "Crankshaft" }, ], }, ]; assert.equal(runtime.normalizeForSearch("Crème brûlée gear-box"), "creme brulee gear box"); assert.deepEqual(runtime.tokenizeForSearch("gears").slice(0, 2), ["gears", "gear"]); const index = runtime.buildMachineSearchIndex(machines); assert.equal(index.length, machines.length); const epicyclicResults = runtime.searchMachines(index, "epicyclic planet", { limit: 3 }); assert.equal(epicyclicResults[0]?.machine.id, "planetary-gearset"); assert(epicyclicResults[0].matchedFields.includes("facts") || epicyclicResults[0].matchedFields.includes("title")); const typoResults = runtime.searchMachines(index, "impeler volute", { limit: 2 }); assert.equal(typoResults[0]?.machine.id, "centrifugal-pump"); const pumpResults = runtime.searchMachines(index, "", { filters: { categories: ["pumps-fluid"] }, limit: Infinity, }); assert.deepEqual( pumpResults.map((result) => result.machine.id), ["centrifugal-pump"], ); const facets = runtime.getCatalogueFacets(index, { filters: { categories: ["gearboxes-drives", "mechanisms"] }, }); assert.equal(facets.total, 3); assert.equal(facets.categories.find((bucket) => bucket.value === "gearboxes-drives")?.count, 2); assert.equal(facets.categories.find((bucket) => bucket.value === "mechanisms")?.count, 1); const related = runtime.getRelatedMachineRecommendations(index, "planetary-gearset", { limit: 3 }); assert.equal(related[0]?.machine.id, "automatic-transmission"); assert.equal(related[0]?.explicit, true); assert(related[0].reasons.some((reason) => reason.includes("Explicitly"))); const opacity = Object.create(null); opacity["ring-gear"] = 0.35; opacity.pinions = 1.4; opacity.__proto__ = 0.1; const codecOptions = { allowedMachineIds: machines.map((machine) => machine.id), allowedComponentIds: [ "sun-gear", "ring-gear", "carrier", "pinions", "impeller", "volute", ], allowedCameraPresetIds: ["power-flow"], allowedTourStepIds: ["torque-path"], }; const sanitized = runtime.sanitizeMachineViewerState( { machineId: "planetary-gearset", cameraPresetId: "power-flow", mode: "cross-section", exploded: 0.625, playback: "playing", speed: 1.75, selectedComponentId: "ring-gear", hiddenComponents: ["carrier", "pinions", "carrier"], componentOpacity: opacity, labels: false, annotations: true, camera: { position: [3.1234567, 2, 5], target: [0, 0, 0], zoom: 1.2, }, section: { axis: "y", offset: 0.25 }, tourStepId: "torque-path", }, codecOptions, ); assert(sanitized.state); assert(sanitized.warnings.length >= 2); assert.deepEqual(sanitized.state.hiddenComponents, ["carrier", "pinions"]); assert.equal(sanitized.state.componentOpacity["ring-gear"], 0.35); assert.equal(sanitized.state.componentOpacity.pinions, 1); assert.equal(Object.prototype.hasOwnProperty.call(sanitized.state.componentOpacity, "__proto__"), false); const params = runtime.encodeMachineViewerState(sanitized.state, codecOptions); assert.equal(params.get("m"), "planetary-gearset"); assert.equal(params.get("mode"), "cross-section"); assert.equal(params.get("labels"), "0"); const decoded = runtime.decodeMachineViewerState(params, codecOptions); assert.deepEqual(decoded.warnings, []); assert.equal(decoded.state?.machineId, "planetary-gearset"); assert.equal(decoded.state?.mode, "cross-section"); assert.equal(decoded.state?.playback, "playing"); assert.equal(decoded.state?.labels, false); assert.equal(decoded.state?.componentOpacity["ring-gear"], 0.35); assert.equal(decoded.state?.componentOpacity.pinions, 1); assert.equal(decoded.state?.camera?.position?.[0], 3.1235); assert.equal(decoded.state?.section?.axis, "y"); const malformed = runtime.decodeMachineViewerState( "?m=planetary-gearset&mode=glass&exp=99&speed=-4&hide=ring-gear,bad id&op=ring-gear:not-a-number,carrier:0.2&cam=1,2,nope&sec=q:99", codecOptions, ); assert(malformed.state); assert(malformed.warnings.length >= 5); assert.equal(malformed.state.mode, "solid"); assert.equal(malformed.state.exploded, 1); assert.equal(malformed.state.speed, 0); assert.deepEqual(malformed.state.hiddenComponents, ["ring-gear"]); assert.equal(malformed.state.componentOpacity.carrier, 0.2); assert.equal(malformed.state.camera, undefined); assert.equal(malformed.state.section, undefined); const viewerUrl = runtime.createMachineViewerUrl( "/explore?utm=docs&m=old#overview", { machineId: "centrifugal-pump", mode: "wireframe", exploded: 0.5, }, ); assert.equal(viewerUrl, "/explore?utm=docs&m=centrifugal-pump&mode=wireframe&exp=0.5#overview"); console.log("catalogue runtime utilities tests passed"); } finally { await rm(tempDir, { recursive: true, force: true }); } async function loadRuntimeModule() { let esbuild; try { esbuild = await import("esbuild"); } catch (error) { throw new Error( "Unable to import esbuild. Run npm install before executing scripts/test-catalogue-runtime-utilities.mjs.", { cause: error }, ); } const outfile = path.join(tempDir, "catalogueRuntime.mjs"); await esbuild.build({ entryPoints: [path.join(repoRoot, "src/modules/machines/catalogue/catalogueRuntime.ts")], bundle: true, platform: "node", format: "esm", target: "node20", outfile, logLevel: "silent", }); return import(`${pathToFileURL(outfile).href}?cacheBust=${Date.now()}`); }