Files
zalbot/tests/unit/loreResolver.test.ts
Kaysser Kayyali fdf1d705d1 fix: lobby close UI + loreResolver skips index chunks (3 live-run bugs)
Bug 1 — loreResolver no longer uses lore-index chunks as randomizable values.
A disposition placeholder resolved to the 'All NPCs' index (Overview + [[wiki-
links]]), corrupting the opening narrative. Filter out index-like chunks
(Overview/Entries/Index header OR >=2 [[links]]) from candidates; fall back if
only index chunks remain. TDD: tests/unit/loreResolver.test.ts (4 tests).
(Follow-up: short-value randomizables like dispositions should use
source:vocabulary — the engine guard is the safety net.)

Bug 2 — begin announcement now precedes the opening narrative. handleStartBtn
uses interaction.update (edits the lobby embed to the announcement) BEFORE
beginEncounter posts the opening, so the thread reads announcement -> opening.

Bug 3 — lobby close updates the UI + renames the thread. On Begin: the lobby
embed is closed in place (Join/Begin buttons removed, replaced by the
announcement) + the thread is renamed '... — underway'. On Cancel: the thread
is renamed '... — cancelled' before archive. Live test AC-8 asserts the embed
is closed (no buttons + announcement) + the thread rename.

Verified: tsc clean, 543 unit tests pass (+4), live multiplayer E2E 3/3.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-22 22:09:42 +00:00

66 lines
2.7 KiB
TypeScript

import { vi, describe, it, expect, beforeEach } from 'vitest';
const { mockSemanticSearch } = vi.hoisted(() => ({ mockSemanticSearch: vi.fn() }));
vi.mock('../../src/graphmcp/client.js', () => ({
semanticSearch: mockSemanticSearch,
queryAsNPC: vi.fn(),
formatNPCMemory: vi.fn(),
logEncounter: vi.fn().mockResolvedValue({}),
}));
const { mockVocab } = vi.hoisted(() => ({ mockVocab: vi.fn() }));
vi.mock('../../src/graphmcp/vocabularyResolver.js', () => ({
sampleFromVocabulary: mockVocab,
}));
vi.mock('../../src/config.js', () => ({
config: { GRAPHMCP_NPC_MEMORY_LIMIT: 5, GRAPHMCP_SCORE_THRESHOLD: 0.68 },
}));
import { resolveRandomizables } from '../../src/graphmcp/loreResolver.js';
beforeEach(() => {
mockSemanticSearch.mockReset();
mockVocab.mockReset();
});
describe('resolveRandomizables — skips lore-index chunks (Bug 1)', () => {
it('does NOT use a lore-index chunk (Overview + [[links]]) as a value; picks a normal chunk', async () => {
mockSemanticSearch.mockResolvedValue({
chunks: [
{
content:
'Overview All named characters in the Land of Mardonar. Entries Rulers & Nobility - [[King Mardonarious]] — ruler of the land - [[Prince of Mardonia]] — heir',
score: 0.95,
},
{ content: 'drunk, and looking for the people who cheated him at dice three weeks back', score: 0.7 },
],
});
const out = await resolveRandomizables([{ key: 'disp', query: 'a reason', fallback: 'drunk' }]);
expect(out.disp).toContain('drunk');
expect(out.disp, 'no wiki-link index text leaks into the value').not.toContain('[[');
expect(out.disp).not.toMatch(/^Overview/);
});
it('falls back when only index-like chunks are returned', async () => {
mockSemanticSearch.mockResolvedValue({
chunks: [{ content: 'Entries - [[A]] - [[B]] - [[C]]', score: 0.9 }],
});
const out = await resolveRandomizables([{ key: 'disp', query: 'x', fallback: 'sober' }]);
expect(out.disp).toBe('sober');
});
it('uses a normal lore chunk (a relic description) as a value', async () => {
mockSemanticSearch.mockResolvedValue({
chunks: [{ content: 'the Crimson Relic of Emberloc, a bloodstone carved with ancestral battle runes', score: 0.8 }],
});
const out = await resolveRandomizables([{ key: 'item', query: 'a relic', fallback: 'a stone' }]);
expect(out.item).toContain('Crimson Relic');
});
it('falls back when semanticSearch returns nothing above threshold', async () => {
mockSemanticSearch.mockResolvedValue({ chunks: [{ content: 'x', score: 0.2 }] });
const out = await resolveRandomizables([{ key: 'k', query: 'q', fallback: 'fb' }]);
expect(out.k).toBe('fb');
});
});