Files
eden/tests/inventory-containment.test.ts
T

68 lines
4.4 KiB
TypeScript

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']));
});
});