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>
66 lines
2.7 KiB
TypeScript
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');
|
|
});
|
|
}); |