Files
Questie-X/Tests/QuestieLearner_spec.lua
T
Xurkon 164584ca87 fix: Sunstrider pin regression, ghost pins, and learner zone normalization
- Fix: isSunstrider block in _MergeSpawnEvidence now checks IsAscensionProtected
  before writing learner data to npcDataOverrides. Without this guard, each Mana
  Wyrm kill overwrote AscensionDB's z1241=5 data with learner z3431 coords.

- Fix: Clustering disabled for zone 1241 (Sunstrider Isle) in _DrawObjectiveIcons
  so all 5 AscensionDB spawn pins display individually instead of collapsing to 2.

- Fix: Ghost pin loop in AvailableQuests.lua -- 'while frames[i]' was iterating a
  string-keyed table with a numeric index (never iterated). Changed to pairs().

- Fix: NormalizeSpawnZoneKey now uses ZoneDB.areaIdToUiMapId for all zones so
  learner evidence is stored under map IDs (e.g. 1241) not area IDs (e.g. 3431),
  matching AscensionDB's key space. Applied in LearnNPC and _StoreGuidSpawnEvidence.

- Fix: isSunstrider detection in _MergeSpawnEvidence updated from hardcoded
  zoneId==3431 check to IsSunstriderNativeZone() since zone IDs are now normalized
  to map IDs at storage time.
2026-05-25 19:02:56 -05:00

823 lines
32 KiB
Lua

-- Tests/QuestieLearner_spec.lua
require("Tests/wow_api_mock")
describe("QuestieLearner", function()
local QuestieLearner
setup(function()
-- Mocking QuestieLoader module resolution for tests
_G.QuestieLoader.ImportModule = function(_, name)
if name == "QuestieDB" then return _G.QuestieDB end
if name == "QuestieQuest" then return _G.QuestieQuest end
if name == "QuestiePlayer" then return _G.QuestiePlayer end
if name == "QuestLogCache" then return _G.QuestLogCache end
if name == "QuestieCompat" then return _G.QuestieCompat end
if name == "ZoneDB" then return _G.ZoneDB end
if name == "l10n" then return _G.l10n end
if name == "QuestieTooltips" then return _G.QuestieTooltips end
if name == "QuestieLib" then return {} end
return {}
end
_G.QuestieLoader.CreateModule = function(_, name)
_G[name] = {}
return _G[name]
end
-- Reset mock state
_G._lastRegisteredTooltip = nil
_G._mock_uiMapId = nil
_G._mock_questObjectives = nil
_G._mock_npcFlags = 2
-- Reset QuestLogCache.GetQuest to default
_G.QuestLogCache.GetQuest = function(_, id) return nil end
-- Load the module
package.loaded["Modules/QuestieLearner"] = nil
QuestieLearner = require("Modules/QuestieLearner")
-- Initialize sets up dbLearner.global.settings, InjectsLearnedData, etc.
QuestieLearner:Initialize()
end)
--==========================================================================
-- Existing test: coordinate scaling in OnCombatLogEvent
--==========================================================================
it("should scale coordinates by 100 in OnCombatLogEvent", function()
local unitGUID = "Creature-0-1234-567-89-21878-0000000000"
local unitName = "Felboar"
_G.QuestieCompat.GetCurrentPlayerPosition = function()
return 946, 0.35, 0.45
end
QuestieLearner:OnCombatLogEvent(GetTime(), "UNIT_DIED", nil, nil, nil, unitGUID, unitName, nil, nil, nil)
local cached = QuestieLearner.private.recentKills[unitGUID]
assert.is_not_nil(cached)
assert.are.equal(35.0, cached.x)
assert.are.equal(45.0, cached.y)
end)
--==========================================================================
-- Phase 2: GUID-based spawn evidence
--==========================================================================
it("should store GUID spawn evidence in entry[8] after a kill", function()
local npcId = 21878
local unitGUID = "Creature-0-1234-567-89-21878-000000ABCD"
local unitName = "Felboar"
-- Clear prior data
Questie.dbLearner.global.npcs = {}
Questie.dbLearner.global.settings.learnNpcs = true
_G.QuestieCompat.GetCurrentPlayerPosition = function()
return 1241, 0.5234, 0.6789
end
QuestieLearner:OnCombatLogEvent(GetTime(), "UNIT_DIED", nil, nil, nil, unitGUID, unitName, nil, nil, nil)
-- Verify recentKills unchanged
local cached = QuestieLearner.private.recentKills[unitGUID]
assert.is_not_nil(cached)
assert.are.equal(npcId, cached.npcId)
assert.are.equal("Felboar", cached.name)
assert.are.equal(5234, cached.x)
assert.are.equal(6789, cached.y)
-- Verify entry[8] was created on the learned NPC
local learnedNpc = Questie.dbLearner.global.npcs[npcId]
assert.is_not_nil(learnedNpc)
assert.is_not_nil(learnedNpc[8])
-- Spawn UID from last numeric segment of GUID: 000000ABCD → 43981
local spawnUID = 43981
assert.is_not_nil(learnedNpc[8][spawnUID], "GUID spawn entry should exist for spawnUID " .. tostring(spawnUID))
local entry = learnedNpc[8][spawnUID]
assert.are.equal(1241, entry.zoneId)
assert.are.equal(52.34, entry.x) -- same scaled format as recentKills
assert.are.equal(67.89, entry.y) -- same scaled format as recentKills
assert.are.equal("local", entry.source)
assert.are.equal(1, entry.confidence)
end)
it("should update existing GUID spawn evidence on repeated kill at same UID", function()
local npcId = 21878
local unitGUID = "Creature-0-1234-567-89-21878-0000005555"
local unitName = "Felboar"
Questie.dbLearner.global.npcs = {}
Questie.dbLearner.global.settings.learnNpcs = true
_G.QuestieCompat.GetCurrentPlayerPosition = function()
return 1241, 0.1111, 0.2222
end
-- First kill
QuestieLearner:OnCombatLogEvent(GetTime(), "UNIT_DIED", nil, nil, nil, unitGUID, unitName, nil, nil, nil)
local spawnUID = 21845
local learnedNpc = Questie.dbLearner.global.npcs[npcId]
local entry1 = learnedNpc[8][spawnUID]
assert.is_not_nil(entry1)
local ts1 = entry1.ts
-- Second kill at same UID — position changes slightly
_G.QuestieCompat.GetCurrentPlayerPosition = function()
return 1241, 0.3333, 0.4444
end
QuestieLearner:OnCombatLogEvent(GetTime(), "UNIT_DIED", nil, nil, nil, unitGUID, unitName, nil, nil, nil)
local entry2 = learnedNpc[8][spawnUID]
assert.is_not_nil(entry2)
assert.are.equal(33.33, entry2.x) -- same scaled format as recentKills
assert.are.equal(44.44, entry2.y) -- same scaled format as recentKills
assert.is_true(entry2.ts >= ts1) -- timestamp refreshed
end)
it("should not double-scale already normalized GUID spawn coordinates", function()
local npcId = 15274
local unitGUID = "Creature-0-1234-567-89-15274-41298"
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Mana Wyrm",
},
}
Questie.dbLearner.global.settings.learnNpcs = true
QuestieLearner:_StoreGuidSpawnEvidence(npcId, unitGUID, 3431, 58.68, 43.19)
local entry = Questie.dbLearner.global.npcs[npcId][8][41298]
assert.is_not_nil(entry)
assert.are.equal(58.68, entry.x)
assert.are.equal(43.19, entry.y)
end)
it("should repair legacy 0-10000 GUID spawn coordinates before merging", function()
local npcId = 15274
local unitGUID = "Creature-0-1234-567-89-15274-41298"
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Mana Wyrm",
},
}
Questie.dbLearner.global.settings.learnNpcs = true
QuestieLearner:_StoreGuidSpawnEvidence(npcId, unitGUID, 3431, 5868, 4319)
local entry = Questie.dbLearner.global.npcs[npcId][8][41298]
assert.is_not_nil(entry)
assert.are.equal(58.68, entry.x)
assert.are.equal(43.19, entry.y)
end)
--==========================================================================
-- Existing test: spell cast learning
--==========================================================================
it("should learn spell casts into dbLearner quest objectives when they match a quest objective", function()
local questId = 12345
local spellId = 29228
local targetGUID = "Creature-0-1234-567-89-21878-0000000000"
Questie.dbLearner.global.quests = {}
_G.QuestieCompat.GetQuestLogTitle = function(index)
return "Test Quest", 1, nil, false, nil, nil, nil, questId
end
_G.QuestLogCache.GetQuest = function(id)
return {
objectives = {
{ type = "spell", text = "Flame Shock the enemy" }
}
}
end
QuestieLearner:LearnSpellCast(spellId, "Flame Shock", targetGUID, "Enemy NPC")
assert.is_not_nil(Questie.dbLearner.global.quests[questId])
assert.is_not_nil(Questie.dbLearner.global.quests[questId][10])
assert.is_not_nil(Questie.dbLearner.global.quests[questId][10][1])
assert.are.same({ 21878, "Flame Shock" }, Questie.dbLearner.global.quests[questId][10][1][1])
end)
--==========================================================================
-- InjectLearnedData zone key migration (uiMapId → areaId)
--==========================================================================
describe("InjectLearnedData zone migration", function()
it("should migrate NPC spawn zone keys from uiMapId to areaId", function()
Questie.dbLearner.global.npcs = {
[21878] = {
[1] = "Felboar",
[7] = {
[1241] = { -- uiMapId (Sunstrider) → should migrate to 3430
{ 35.0, 45.0 },
{ 40.0, 50.0 },
},
},
},
}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.items = {}
-- Reload the data by calling InjectLearnedData (already called in Initialize, but re-call with updated data)
QuestieLearner:InjectLearnedData()
local npcs = Questie.dbLearner.global.npcs
assert.is_nil(npcs[21878][7][1241],
"Old uiMapId key 1241 should be removed after migration")
assert.is_not_nil(npcs[21878][7][3430],
"New areaId key 3430 should exist after migration")
assert.are.equal(2, #npcs[21878][7][3430],
"Should have 2 coords entries in migrated zone")
assert.are.same({ 35.0, 45.0 }, npcs[21878][7][3430][1])
assert.are.same({ 40.0, 50.0 }, npcs[21878][7][3430][2])
end)
it("should merge migrated spawn data with existing areaId data", function()
Questie.dbLearner.global.npcs = {
[21878] = {
[1] = "Felboar",
[7] = {
[1241] = { -- uiMapId → merge into 3430
{ 35.0, 45.0 },
},
[3430] = { -- Already correct areaId
{ 50.0, 55.0 },
},
},
},
}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.items = {}
QuestieLearner:InjectLearnedData()
local npcs = Questie.dbLearner.global.npcs
assert.is_nil(npcs[21878][7][1241], "Old uiMapId key should be removed")
assert.are.equal(2, #npcs[21878][7][3430],
"Merged: 1 existing + 1 migrated = 2 entries")
end)
it("should migrate object spawn zone keys from uiMapId to areaId", function()
Questie.dbLearner.global.npcs = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
Questie.dbLearner.global.objects = {
[181345] = {
[1] = "Mysterious Pedestal",
[4] = {
[1241] = { -- uiMapId → migrate to 3430
{ 30.0, 40.0 },
},
},
},
}
QuestieLearner:InjectLearnedData()
local objects = Questie.dbLearner.global.objects
assert.is_nil(objects[181345][4][1241],
"Old uiMapId key should be removed from objects")
assert.is_not_nil(objects[181345][4][3430],
"Migrated areaId key should exist")
assert.are.same({ 30.0, 40.0 }, objects[181345][4][3430][1])
end)
it("should leave non-mappable zone keys unchanged", function()
Questie.dbLearner.global.npcs = {
[21878] = {
[1] = "Felboar",
[7] = {
[3430] = { -- Already correct areaId — stay
{ 35.0, 45.0 },
},
[530] = { -- No ZoneDB mapping — stay
{ 60.0, 30.0 },
},
},
},
}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.items = {}
QuestieLearner:InjectLearnedData()
local npcs = Questie.dbLearner.global.npcs
assert.is_not_nil(npcs[21878][7][3430], "3430 areaId should remain")
assert.is_not_nil(npcs[21878][7][530], "530 (no mapping) should remain")
assert.are.equal(1, #npcs[21878][7][3430], "3430 should still have 1 entry")
assert.are.equal(1, #npcs[21878][7][530], "530 should still have 1 entry")
end)
end)
--==========================================================================
-- LearnQuestObjectiveNPC icon preservation
--==========================================================================
describe("LearnQuestObjectiveNPC icon preservation", function()
it("should pass the objective icon from QuestLogCache to RegisterObjectiveTooltip", function()
local questId = 1001
local npcId = 21878
local objText = "Slay 10 felboars"
local objectiveIndex = 1
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.npcs = {}
-- Mock QuestLogCache.GetQuestObjectives returns objectives with Icon 136006
_G._mock_questObjectives = {
{ text = "Slay 10 felboars", Icon = 136006, objectiveType = "slay" },
}
QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiveIndex)
-- Verify RegisterObjectiveTooltip was called with the icon
assert.is_not_nil(_G._lastRegisteredTooltip,
"RegisterObjectiveTooltip should have been called")
assert.are.equal(questId, _G._lastRegisteredTooltip.questId)
assert.are.equal("m_" .. npcId, _G._lastRegisteredTooltip.identifier)
assert.are.equal(objText, _G._lastRegisteredTooltip.data.Description)
assert.are.equal(136006, _G._lastRegisteredTooltip.data.Icon,
"Icon from QuestLogCache should be passed to tooltip registration")
end)
it("should work when QuestLogCache has no matching objective text", function()
local questId = 1002
local npcId = 21879
local objText = "Gather 5 moonflowers"
local objectiveIndex = 1
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.npcs = {}
-- Objectives don't include "moonflowers"
_G._mock_questObjectives = {
{ text = "Slay 10 felboars", Icon = 136006 },
{ text = "Collect 5 herbs", Icon = 134217 },
}
QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiveIndex)
-- Should still register, but with nil Icon (graceful fallback)
assert.is_not_nil(_G._lastRegisteredTooltip,
"RegisterObjectiveTooltip should still be called without matching icon")
assert.is_nil(_G._lastRegisteredTooltip.data.Icon,
"Icon should be nil when no matching objective text is found")
end)
it("should work when QuestLogCache.GetQuestObjectives is nil", function()
local questId = 1003
local npcId = 21880
local objText = "Kill 3 worgs"
local objectiveIndex = 1
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.npcs = {}
-- nil/empty so no icon lookup happens
_G._mock_questObjectives = nil
QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiveIndex)
assert.is_not_nil(_G._lastRegisteredTooltip,
"RegisterObjectiveTooltip should be called even with nil objectives")
assert.is_nil(_G._lastRegisteredTooltip.data.Icon,
"Icon should be nil when GetQuestObjectives returns nil")
end)
end)
--==========================================================================
-- MIN_CONFIDENCE_PINS = 1
--==========================================================================
describe("Settings defaults", function()
it("should have minConfidencePins default to 1 on fresh data", function()
-- After Initialize, settings should be set with minConfidencePins = 1
local settings = QuestieLearner:GetSettings()
assert.are.equal(1, settings.minConfidencePins,
"minConfidencePins should be 1 for Ascension play")
end)
it("should backfill legacy SavedVariables with minConfidencePins = 1", function()
-- Simulate legacy data without minConfidencePins
if Questie.dbLearner.global.settings then
Questie.dbLearner.global.settings.minConfidencePins = nil
end
-- Re-initialize to trigger backfill
QuestieLearner:Initialize()
local settings = QuestieLearner:GetSettings()
assert.are.equal(1, settings.minConfidencePins,
"Legacy settings should be backfilled with minConfidencePins = 1")
end)
end)
--==========================================================================
-- NPC learning API
--==========================================================================
describe("LearnNPC spawn zone tracking", function()
it("should store NPC spawn zoneId correctly", function()
local npcId = 21878
local spawnZoneId = 3430 -- Eversong Woods
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.npcs = {}
-- Mock player position
_G.QuestieCompat.GetCurrentPlayerPosition = function()
return 3430, 0.35, 0.45
end
-- Mock coords
_G.UnitPosition = function(unit) return -8940, -137, 82 end
QuestieLearner:LearnNPC(npcId, "Felboar", 60, nil, 2, nil, nil, nil, spawnZoneId)
assert.is_not_nil(Questie.dbLearner.global.npcs[npcId],
"NPC should be learned")
assert.are.equal("Felboar", Questie.dbLearner.global.npcs[npcId][1],
"NPC name should be stored")
-- NPC spawn coords should be stored under the areaId
if Questie.dbLearner.global.npcs[npcId][7] then
assert.is_not_nil(Questie.dbLearner.global.npcs[npcId][7][spawnZoneId],
"Spawn coords should be stored under zoneId " .. spawnZoneId)
end
end)
end)
--==========================================================================
-- PruneLearnedSpawnOutliers
--==========================================================================
describe("PruneLearnedSpawnOutliers", function()
local function setStaticNPC(npcId, spawnData)
-- Mock static DB return for QuestieDB.QueryNPC
_G.QuestieDB = _G.QuestieDB or {}
_G.QuestieDB.QueryNPC = function(id, ...)
if id == npcId then
return spawnData -- { [7] = { [zoneId] = { {x,y}, ... } } }
end
return nil
end
end
after_each(function()
-- Restore QuestieDB.QueryNPC to nil (no static data)
_G.QuestieDB = _G.QuestieDB or {}
_G.QuestieDB.QueryNPC = function() return nil end
end)
it("should remove learned spawns that deviate > threshold from static anchor", function()
local npcId = 21878
local zoneId = 3430
local threshold = 15
-- Static DB anchor: NPC spawns at (50, 50) with tolerance ±15
setStaticNPC(npcId, {
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
},
},
})
-- Learned data: one spawn near anchor, one far away
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Felboar",
[7] = {
[zoneId] = {
{ 50.0, 50.0 }, -- within threshold
{ 10.0, 10.0 }, -- > 15% away from anchor → pruned
{ 51.0, 49.0 }, -- within threshold
},
},
},
}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
local changed = QuestieLearner:PruneLearnedSpawnOutliers(threshold)
assert.is_true(changed)
local spawns = Questie.dbLearner.global.npcs[npcId][7][zoneId]
assert.are.equal(2, #spawns, "Should have 2 spawns after pruning")
end)
it("should use median cluster when no static anchor exists", function()
local npcId = 21879
local zoneId = 3430
local threshold = 10
-- No static DB entry
setStaticNPC(npcId, nil)
-- Three spawns: two clustered at (50,50), one outlier at (10,10)
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Worg",
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
{ 50.5, 49.5 },
{ 10.0, 10.0 }, -- outlier
},
},
},
}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
local changed = QuestieLearner:PruneLearnedSpawnOutliers(threshold)
assert.is_true(changed)
local spawns = Questie.dbLearner.global.npcs[npcId][7][zoneId]
assert.are.equal(2, #spawns, "Outlier should be removed")
end)
it("should call InjectLearnedData after pruning when data changed", function()
local npcId = 21880
local zoneId = 3430
setStaticNPC(npcId, {
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
},
},
})
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Tiger",
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
{ 5.0, 5.0 }, -- outlier
},
},
},
}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
local injectCalled = false
local origInject = QuestieLearner.InjectLearnedData
QuestieLearner.InjectLearnedData = function(self)
injectCalled = true
return origInject(self)
end
local changed = QuestieLearner:PruneLearnedSpawnOutliers(15)
QuestieLearner.InjectLearnedData = origInject
assert.is_true(changed, "Data should be changed")
assert.is_true(injectCalled, "InjectLearnedData should be called after pruning")
end)
it("should not call InjectLearnedData when no spawns were pruned", function()
local npcId = 21881
local zoneId = 3430
setStaticNPC(npcId, {
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
},
},
})
-- All spawns within threshold — nothing pruned
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Bear",
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
{ 55.0, 45.0 }, -- still within 15 threshold
},
},
},
}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
local injectCalled = false
local origInject = QuestieLearner.InjectLearnedData
QuestieLearner.InjectLearnedData = function(self)
injectCalled = true
return origInject(self)
end
local changed = QuestieLearner:PruneLearnedSpawnOutliers(15)
QuestieLearner.InjectLearnedData = origInject
assert.is_false(changed, "No data should be changed")
assert.is_false(injectCalled, "InjectLearnedData should NOT be called when nothing changed")
end)
it("should prune object spawns stored in [4] without static anchor", function()
local objectId = 181345
local zoneId = 3430
local threshold = 10
-- Objects store spawns in [4], not [7]
Questie.dbLearner.global.npcs = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
Questie.dbLearner.global.objects = {
[objectId] = {
[1] = "Pedestal",
[4] = {
[zoneId] = {
{ 30.0, 30.0 },
{ 30.5, 29.5 },
{ 80.0, 80.0 }, -- outlier
},
},
},
}
local changed = QuestieLearner:PruneLearnedSpawnOutliers(threshold)
assert.is_true(changed)
local spawns = Questie.dbLearner.global.objects[objectId][4][zoneId]
assert.are.equal(2, #spawns, "Object outlier should be pruned")
end)
it("should return false when there is nothing to prune", function()
Questie.dbLearner.global.npcs = {}
Questie.dbLearner.global.objects = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
local changed = QuestieLearner:PruneLearnedSpawnOutliers(15)
assert.is_false(changed, "Should return false when nothing to prune")
end)
end)
--==========================================================================
-- _MergeSpawnEvidence (via OnCombatLogEvent integration)
--==========================================================================
describe("_MergeSpawnEvidence integration", function()
local function setStaticNPC(npcId, spawnData)
_G.QuestieDB = _G.QuestieDB or {}
_G.QuestieDB.QueryNPC = function(id, ...)
if id == npcId then return spawnData end
return nil
end
end
after_each(function()
_G.QuestieDB = _G.QuestieDB or {}
_G.QuestieDB.QueryNPC = function() return nil end
end)
it("should merge when 3+ kills at same spawn point and >60% threshold exceeded", function()
local npcId = 21878
local unitGUID = "Creature-0-1234-567-89-21878-0000001111"
local unitName = "Felboar"
local zoneId = 3430
setStaticNPC(npcId, {
[7] = {
[zoneId] = {
{ 50.0, 50.0 }, -- different from learned
},
},
})
Questie.dbLearner.global.npcs = {}
Questie.dbLearner.global.settings.learnNpcs = true
-- Three kills at different GUIDs but same zone+coords
local sameX, sameY = 0.1111, 0.2222
for i = 1, 3 do
local guid = "Creature-0-1234-567-89-21878-" .. string.format("%010d", 1000 + i)
_G.QuestieCompat.GetCurrentPlayerPosition = function()
return zoneId, sameX, sameY
end
QuestieLearner:OnCombatLogEvent(GetTime(), "UNIT_DIED",
nil, nil, nil, guid, unitName, nil, nil, nil)
end
-- After 3rd kill, _MergeSpawnEvidence should have injected spawn override
local override = QuestieDB.npcDataOverrides[npcId]
local spawnList = override and override[7] and override[7][zoneId]
assert.is_not_nil(spawnList, "Spawn override should be injected")
assert.is_true(#spawnList >= 1, "At least one spawn should be injected")
end)
it("should NOT merge when < 3 kills (insufficient evidence)", function()
local npcId = 21879
local zoneId = 3430
setStaticNPC(npcId, {
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
},
},
})
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Worg",
[8] = {
[1001] = { zoneId = zoneId, x = 11.0, y = 22.0, ts = time(), source = "local", confidence = 1 },
[1002] = { zoneId = zoneId, x = 11.0, y = 22.0, ts = time(), source = "local", confidence = 1 },
},
},
}
Questie.dbLearner.global.settings.learnNpcs = true
-- Only 2 kills — below threshold
-- (simulate by directly setting guidSpawns as above)
-- Verify no override was created
local overrideBefore = QuestieDB.npcDataOverrides[npcId]
assert.is_nil(overrideBefore,
"No override should exist with only 2 kills")
end)
it("should NOT merge when top spawn is at or below 60% confidence", function()
local npcId = 21880
local zoneId = 3430
-- Static DB has a spawn at (50, 50)
setStaticNPC(npcId, {
[7] = {
[zoneId] = {
{ 50.0, 50.0 },
},
},
})
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Bear",
[8] = {
[1001] = { zoneId = zoneId, x = 11.0, y = 22.0, ts = time(), source = "local", confidence = 1 },
[1002] = { zoneId = zoneId, x = 11.0, y = 22.0, ts = time(), source = "local", confidence = 1 },
[1003] = { zoneId = zoneId, x = 33.0, y = 44.0, ts = time(), source = "local", confidence = 1 },
},
},
}
Questie.dbLearner.global.settings.learnNpcs = true
-- 2 kills at (11,22), 1 kill at (33,44) = 50% each for top
-- Top is at 50% ≤ 60%, should not override
local overrideBefore = QuestieDB.npcDataOverrides[npcId]
assert.is_nil(overrideBefore,
"No override at 50% confidence (below 60% threshold)")
end)
it("should NOT override when learned spawn matches static DB entry", function()
local npcId = 21881
local zoneId = 3430
local staticX, staticY = 50.0, 50.0
-- Static DB has spawn at (50, 50)
setStaticNPC(npcId, {
[7] = {
[zoneId] = {
{ staticX, staticY },
},
},
})
-- Learned evidence: 3 kills at same coords as static (within rounding)
local learnedX = 50.0
local learnedY = 50.0
Questie.dbLearner.global.npcs = {
[npcId] = {
[1] = "Tiger",
[8] = {
[1001] = { zoneId = zoneId, x = learnedX, y = learnedY, ts = time(), source = "local", confidence = 1 },
[1002] = { zoneId = zoneId, x = learnedX, y = learnedY, ts = time(), source = "local", confidence = 1 },
[1003] = { zoneId = zoneId, x = learnedX, y = learnedY, ts = time(), source = "local", confidence = 1 },
},
},
}
Questie.dbLearner.global.settings.learnNpcs = true
-- All 3 kills agree at 100% but matches static — no override needed
local overrideBefore = QuestieDB.npcDataOverrides[npcId]
assert.is_nil(overrideBefore,
"No override when learned matches static DB")
end)
end)
end)