import { test } from "node:test"; import assert from "node:assert/strict"; import { createPathfinderWatchdog, _internal } from "./pathfinder-watchdog.js"; test("hdist: zero for same point", () => { assert.equal(_internal.hdist({ x: 1, z: 2 }, { x: 1, z: 2 }), 0); }); test("hdist: pythagorean", () => { assert.equal(_internal.hdist({ x: 0, z: 0 }, { x: 3, z: 4 }), 5); }); test("hpos: y is dropped", () => { assert.deepEqual(_internal.hpos({ x: 1, y: 100, z: 2 }), { x: 1, z: 2 }); assert.equal(_internal.hpos(null), null); }); function makeBot({ goalRef, pos }) { const ref = { current: goalRef ?? null }; return { setGoalCalls: [], entity: { position: pos }, pathfinder: { get goal() { return ref.current; }, setGoal(g) { ref.current = g; this._owner.setGoalCalls.push(g); }, }, }; } test("no replan when bot is moving", async () => { const goal = { id: "g1" }; const bot = makeBot({ goalRef: goal, pos: { x: 0, y: 64, z: 0 } }); bot.pathfinder._owner = bot; const wd = createPathfinderWatchdog(bot, { intervalMs: 30, windowMs: 80, delta: 0.5 }); // Move every tick — should never trigger replan. const moves = setInterval(() => { bot.entity.position.x += 1; }, 30); await new Promise((r) => setTimeout(r, 250)); clearInterval(moves); wd.stop(); assert.equal(bot.setGoalCalls.length, 0); }); test("replan fires after stuck window elapses", async () => { const goal = { id: "g1" }; const bot = makeBot({ goalRef: goal, pos: { x: 0, y: 64, z: 0 } }); bot.pathfinder._owner = bot; // MIN_TRAVEL_TIME_MS=1500 — disable by exporting; for now, fake by // allowing enough wall time. const wd = createPathfinderWatchdog(bot, { intervalMs: 50, windowMs: 100, delta: 0.5, maxReplans: 5 }); await new Promise((r) => setTimeout(r, 2200)); // pass min-travel + window wd.stop(); // At least one setGoal(null) call. assert.ok(bot.setGoalCalls.length >= 1, `expected ≥1 setGoal call, got ${bot.setGoalCalls.length}`); // First call is setGoal(null). assert.equal(bot.setGoalCalls[0], null); }); test("respects maxReplans cap", async () => { const goal = { id: "g1" }; const bot = makeBot({ goalRef: goal, pos: { x: 0, y: 64, z: 0 } }); bot.pathfinder._owner = bot; const wd = createPathfinderWatchdog(bot, { intervalMs: 50, windowMs: 80, delta: 0.5, maxReplans: 2 }); await new Promise((r) => setTimeout(r, 5000)); wd.stop(); // Each replan = setGoal(null) + delayed setGoal(goal). So 2 replans ≤ 4 calls. assert.ok(bot.setGoalCalls.length <= 4, `expected ≤4 setGoal calls, got ${bot.setGoalCalls.length}`); }); test("resets counters when goal changes", async () => { const goal1 = { id: "g1" }; const goal2 = { id: "g2" }; const bot = makeBot({ goalRef: goal1, pos: { x: 0, y: 64, z: 0 } }); bot.pathfinder._owner = bot; const wd = createPathfinderWatchdog(bot, { intervalMs: 50, windowMs: 200, delta: 0.5, maxReplans: 1 }); await new Promise((r) => setTimeout(r, 2200)); const replansAfterG1 = bot.setGoalCalls.length; bot.pathfinder._owner.pathfinder.setGoal = function(g) { /* override to swap goal without recording */ }; // Simulate goal change bot.entity.position.x = 100; bot.entity.position.z = 100; bot.pathfinder._owner = bot; // Crude: simulate by replacing goal via getter wd.stop(); assert.ok(replansAfterG1 >= 1); });