import { readFileSync } from 'node:fs'; import { describe, expect, test } from 'bun:test'; import { resolveContainment, classifyFittedFlag } from '../src/lib/server/inventory/containment'; import type { Asset } from '../src/lib/domain/model'; const nested = JSON.parse(readFileSync('tests/fixtures/inventory/nested-assets.json', 'utf8')) as Asset[]; const cycles = JSON.parse(readFileSync('tests/fixtures/inventory/cycles-and-missing.json', 'utf8')) as Asset[]; describe('T08 containment graph', () => { test('resolves nested character-scoped decimal IDs deterministically', () => { const first = resolveContainment({ assets: [...nested].reverse(), characterId: 42, names: new Map([['9007199254740993', 'Same name'], ['9007199254740995', 'Same name']]), typeNames: new Map([[102, 'Fitted module']]), }); const second = resolveContainment({ assets: nested, characterId: 42, names: new Map([['9007199254740993', 'Same name'], ['9007199254740995', 'Same name']]), typeNames: new Map([[102, 'Fitted module']]) }); expect(first.complete).toBe(true); expect(first.nodes.map((node) => node.itemId)).toEqual(['9007199254740993', '9007199254740995', '9007199254740997']); expect(first.nodes[2]).toMatchObject({ depth: 2, root: { kind: 'external', id: '60003760' }, fitted: { classification: 'fitted', rawFlag: 'HiSlot0' } }); expect(first.nodes[2]?.breadcrumbs.map((entry) => entry.name)).toEqual(['Same name', 'Same name']); expect(first.nodes).toEqual(second.nodes); }); test('preserves ambiguous fit flags rather than guessing', () => { expect(classifyFittedFlag('AutoFit').classification).toBe('unknown'); expect(classifyFittedFlag('SpecializedShipHold').classification).toBe('unknown'); expect(classifyFittedFlag('Cargo').classification).toBe('unknown'); expect(classifyFittedFlag('Hangar').classification).toBe('unknown'); expect(classifyFittedFlag('LoSlot7').classification).toBe('fitted'); }); test('reports cycles, missing parents, duplicate IDs and depth caps', () => { const result = resolveContainment({ assets: cycles, characterId: 42 }); expect(result.complete).toBe(false); expect(result.nodes.find((node) => node.itemId === '10')?.unresolved).toContain('cycle'); expect(result.nodes.find((node) => node.itemId === '11')?.unresolved).toContain('cycle'); expect(result.nodes.find((node) => node.itemId === '12')?.unresolved).toContain('missing_parent'); const duplicate = resolveContainment({ assets: [cycles[0]!, { ...cycles[0]!, typeId: 999 }], characterId: 42 }); expect(duplicate.nodes.every((node) => node.unresolved.includes('duplicate_item_id'))).toBe(true); const capped = resolveContainment({ assets: nested, characterId: 42, maxDepth: 1 }); expect(capped.nodes.find((node) => node.itemId === '9007199254740997')?.unresolved).toContain('depth_cap'); }); test('does not infer an item parent from a non-item numeric location', () => { const result = resolveContainment({ assets: [{ ...nested[1]!, locationId: nested[0]!.itemId, locationType: 'station' }], characterId: 42 }); expect(result.nodes[0]).toMatchObject({ parentItemId: null, root: { kind: 'external', id: nested[0]!.itemId } }); expect(result.complete).toBe(true); }); test('bounds a valid 50k chain without recursive stack growth', () => { const deep = Array.from({ length: 50_000 }, (_, index) => ({ itemId: String(index + 1), characterId: 42, typeId: 100, quantity: 1, locationId: index === 0 ? '60003760' : String(index), locationType: index === 0 ? 'station' : 'item', locationFlag: 'Cargo', })) as Asset[]; const result = resolveContainment({ assets: deep, characterId: 42 }); expect(result.nodes).toHaveLength(50_000); expect(result.complete).toBe(false); expect(result.nodes.at(-1)?.unresolved).toContain('depth_cap'); }); test('returns safe unresolved nodes for null and invalid-ID input', () => { const result = resolveContainment({ assets: [null, { itemId: '0', locationId: 'not-an-id', characterId: 42, typeId: 1, quantity: 1, locationType: 'station', locationFlag: 'Hangar' }] as any, characterId: 42 }); expect(result.nodes[0]?.unresolved).toEqual(expect.arrayContaining(['invalid_asset', 'invalid_item_id', 'invalid_location_id'])); expect(result.nodes[1]?.unresolved).toEqual(expect.arrayContaining(['invalid_asset', 'invalid_item_id', 'invalid_location_id'])); }); });