Newer
Older
rpg / apps / game / tools / guards / __tests__ / boundary.test.mjs
import { describe, expect, it } from 'vitest';
import { checkBoundary, importSpecs } from '../boundary.mjs';

/** Пути в test-параметрах пишутся со слэшем, приводим к системным сепараторам. */
const sep = process.platform === 'win32' ? '\\' : '/';
const files = (entries) => new Map(entries.map(([f, src]) => [f.replaceAll('/', sep), src]));

describe('importSpecs', () => {
    it('вытаскивает статические, side-effect, re-export и динамические импорты', () => {
        const src = [
            "import { a } from './a';",
            "import './b';",
            'import * as ns from "../c/d";',
            "const m = await import('./dynamic');",
            "export { x } from './reexport';"
        ].join('\n');
        expect(importSpecs(src)).toEqual(['./a', './b', '../c/d', './dynamic', './reexport']);
    });
});

describe('checkBoundary: движок', () => {
    it('импорт из игры — error', () => {
        const v = checkBoundary(files([
            ['packages/engine/src/core/x.ts', "import { hero } from '../../../apps/game/src/data/npcs';"]
        ]));
        expect(v).toEqual([
            expect.objectContaining({ severity: 'error', rule: 'engine-no-game' })
        ]);
    });

    it('относительный импорт мимо пакета — error', () => {
        const v = checkBoundary(files([
            ['packages/engine/src/core/x.ts', "import { t } from '../../../CLAUDE.md';"]
        ]));
        expect(v[0]).toMatchObject({ rule: 'engine-self-contained', severity: 'error' });
    });

    it('npm-импорты и внутренние относительные — чисто', () => {
        const v = checkBoundary(files([
            ['packages/engine/src/render/y.ts', "import { Container } from 'pixi.js';\nimport { a } from '../core/a';"]
        ]));
        expect(v).toEqual([]);
    });
});

describe('checkBoundary: игра', () => {
    it('под-путь движка мимо index.ts — error', () => {
        const v = checkBoundary(files([
            ['apps/game/src/systems/z.ts', "import { iso } from '@rpg/engine/math/iso';"]
        ]));
        expect(v[0]).toMatchObject({ rule: 'game-engine-public-api', severity: 'error', spec: '@rpg/engine/math/iso' });
    });

    it('публичное API разрешено: сам пакет и assets/*', () => {
        const v = checkBoundary(files([
            ['apps/game/src/main.ts', "import { Game } from '@rpg/engine';\nimport fontUrl from '@rpg/engine/assets/fonts/VT323-Regular.ttf?url';"]
        ]));
        expect(v).toEqual([]);
    });

    it('относительный импорт в движок мимо API — error', () => {
        const v = checkBoundary(files([
            ['apps/game/src/data/map.ts', "import { rleDecode } from '../../../packages/engine/src/map/mapFormat';"]
        ]));
        expect(v[0]).toMatchObject({ rule: 'game-self-contained', severity: 'error' });
    });

    it('npm-импорты и внутренние относительные — чисто', () => {
        const v = checkBoundary(files([
            ['apps/game/src/scenes/s.ts', "import * as PIXI from 'pixi.js';\nimport { AREAS } from '../data/locations';"]
        ]));
        expect(v).toEqual([]);
    });
});