import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createDraft, type LoadoutDraft } from '$lib/builder/loadout'; const mocks = vi.hoisted(() => ({ insertBuild: vi.fn(), getSessionUser: vi.fn(async () => ({ id: 'agent-1', displayName: 'Test Agent', })), database: { prepare: vi.fn(), }, })); vi.mock('$lib/server/auth/session', () => ({ getSessionUser: mocks.getSessionUser, })); vi.mock('$lib/server/database', () => ({ getDatabase: vi.fn(async () => mocks.database), })); import { actions } from './+page.server'; const primaryStatId = 'skill-duration'; const secondaryStatIds = ['critical-hit-chance', 'critical-hit-damage']; const validLoadout = (): LoadoutDraft => { const loadout = createDraft(); loadout.buildName = 'Validated Skill Chips'; loadout.specialization.focusId = 'focus-1'; loadout.chips[0] = { setId: 'chip-set-1', quality: 'Superior', primary: { statId: primaryStatId, value: 15 }, secondary: [ { statId: secondaryStatIds[0], value: 2 }, { statId: secondaryStatIds[1], value: 4 }, ], }; return loadout; }; const saveRequest = (loadout: LoadoutDraft) => { const form = new FormData(); form.set('loadout', JSON.stringify(loadout)); form.set('visibility', 'private'); return new Request('http://localhost/builder?/save', { method: 'POST', body: form, }); }; const runSave = (loadout: LoadoutDraft) => actions.save?.({ cookies: {} as never, request: saveRequest(loadout), } as never); describe('Builder Skill Chip save validation', () => { beforeEach(() => { vi.clearAllMocks(); mocks.database.prepare.mockImplementation(async (sql: string) => ({ all: vi.fn(async () => { if (sql.includes('FROM skill_chip_sets')) return [ { id: 'chip-set-1', specialization_ability_id: 'ability-1', }, ]; if (sql.includes('FROM specialization_focus_abilities')) return [{ ability_id: 'ability-1', ability_slot: 1 }]; if (sql.includes('FROM skill_chip_primary_options')) return [{ stat_attribute_id: primaryStatId }]; if (sql.includes('FROM skill_chip_primary_quality_values')) return [ { stat_attribute_id: primaryStatId, quality: 'Superior', min_value: 10, max_value: 20, }, ]; if (sql.includes('FROM skill_chip_secondary_options')) return secondaryStatIds.map((stat_attribute_id) => ({ stat_attribute_id, })); if (sql.includes('FROM skill_chip_secondary_quality_values')) return secondaryStatIds.map((stat_attribute_id) => ({ stat_attribute_id, quality: 'Superior', min_value: 1, max_value: 5, })); return []; }), run: mocks.insertBuild, })); }); it('rejects a selected Skill Chip assigned to a different focus ability', async () => { mocks.database.prepare.mockImplementation(async (sql: string) => ({ all: vi.fn(async () => { if (sql.includes('FROM skill_chip_sets')) return [ { id: 'chip-set-1', specialization_ability_id: 'ability-2', }, ]; if (sql.includes('FROM specialization_focus_abilities')) return [{ ability_id: 'ability-1', ability_slot: 1 }]; return []; }), run: mocks.insertBuild, })); const result = await runSave(validLoadout()); expect(result).toMatchObject({ status: 400, data: { buildError: 'Skill Chip 1 is not compatible with its assigned ability.', }, }); expect(mocks.insertBuild).not.toHaveBeenCalled(); }); it('accepts a Skill Chip owned by an explicitly compatible ability', async () => { mocks.database.prepare.mockImplementation(async (sql: string) => ({ all: vi.fn(async () => { if (sql.includes('FROM skill_chip_sets')) return [ { id: 'chip-set-1', specialization_ability_id: 'ability-2', }, ]; if (sql.includes('FROM specialization_ability_skill_chip_links')) return [ { ability_id: 'ability-1', compatible_ability_id: 'ability-2', }, ]; if (sql.includes('FROM specialization_focus_abilities')) return [{ ability_id: 'ability-1', ability_slot: 1 }]; if (sql.includes('FROM skill_chip_primary_options')) return [{ stat_attribute_id: primaryStatId }]; if (sql.includes('FROM skill_chip_primary_quality_values')) return [ { stat_attribute_id: primaryStatId, quality: 'Superior', min_value: 10, max_value: 20, }, ]; if (sql.includes('FROM skill_chip_secondary_options')) return secondaryStatIds.map((stat_attribute_id) => ({ stat_attribute_id, })); if (sql.includes('FROM skill_chip_secondary_quality_values')) return secondaryStatIds.map((stat_attribute_id) => ({ stat_attribute_id, quality: 'Superior', min_value: 1, max_value: 5, })); return []; }), run: mocks.insertBuild, })); const result = await runSave(validLoadout()); expect(result).toMatchObject({ buildSaved: true }); expect(mocks.insertBuild).toHaveBeenCalledOnce(); }); it.each([ { label: 'primary', mutate: (loadout: LoadoutDraft) => { loadout.chips[0].primary.value = 21; }, message: 'Skill Chip 1 has an invalid primary Attribute value.', }, { label: 'secondary', mutate: (loadout: LoadoutDraft) => { loadout.chips[0].secondary[1].value = 6; }, message: 'Skill Chip 1 has an invalid secondary Attribute value.', }, ])('rejects an out-of-range $label value', async ({ mutate, message }) => { const loadout = validLoadout(); mutate(loadout); const result = await runSave(loadout); expect(result).toMatchObject({ status: 400, data: { buildError: message }, }); expect(mocks.insertBuild).not.toHaveBeenCalled(); }); it('rejects duplicate secondary Attributes', async () => { const loadout = validLoadout(); loadout.chips[0].secondary[1].statId = secondaryStatIds[0]; const result = await runSave(loadout); expect(result).toMatchObject({ status: 400, data: { buildError: 'Skill Chip 1 must use distinct secondary Attributes.', }, }); expect(mocks.insertBuild).not.toHaveBeenCalled(); }); it('accepts primary and secondary values within the configured profiles', async () => { const result = await runSave(validLoadout()); expect(result).toMatchObject({ buildSaved: true, buildToken: expect.any(String), savedBuild: { visibility: 'private', title: 'Validated Skill Chips', }, }); expect(mocks.insertBuild).toHaveBeenCalledOnce(); }); it('rejects an equipment Talent tier without canonical tier data', async () => { const loadout = createDraft(); loadout.buildName = 'Invalid Talent Tier'; loadout.gear.Holster.itemId = 'gear-1'; loadout.gear.Holster.talentId = 'talent-1'; loadout.gear.Holster.talentTier = 2; mocks.database.prepare.mockImplementation(async (sql: string) => ({ all: vi.fn(async () => { if (sql.includes('FROM gear_talents')) return [{ gear_id: 'gear-1', talent_id: 'talent-1' }]; if (sql.includes('FROM talent_tiers')) return [{ talent_id: 'talent-1', tier: 'I' }]; return []; }), run: mocks.insertBuild, })); const result = await runSave(loadout); expect(result).toMatchObject({ status: 400, data: { buildError: 'Holster Talent T2 is not configured.' }, }); expect(mocks.insertBuild).not.toHaveBeenCalled(); }); it('saves an equipment Talent with an authored Tier II', async () => { const loadout = createDraft(); loadout.buildName = 'Tier Two Talent'; loadout.gear.Holster.itemId = 'gear-1'; loadout.gear.Holster.talentId = 'talent-1'; loadout.gear.Holster.talentTier = 2; mocks.database.prepare.mockImplementation(async (sql: string) => ({ all: vi.fn(async () => { if (sql.includes('FROM gear_talents')) return [{ gear_id: 'gear-1', talent_id: 'talent-1' }]; if (sql.includes('FROM talent_tiers')) return [ { talent_id: 'talent-1', tier: 'I' }, { talent_id: 'talent-1', tier: 'II' }, ]; return []; }), run: mocks.insertBuild, })); const result = await runSave(loadout); expect(result).toMatchObject({ buildSaved: true }); expect(mocks.insertBuild).toHaveBeenCalledOnce(); }); });