feat(sdk): define single-game public contracts
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { afterEach, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { scanImportBoundaries } from '../../scripts/lib/import-boundaries.mjs';
|
||||
|
||||
const roots = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
async function createFixtureRoot() {
|
||||
const root = await mkdtemp(join(tmpdir(), 'youle-import-boundaries-'));
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
async function writeFixture(root, relativePath, source) {
|
||||
const file = join(root, relativePath);
|
||||
await mkdir(dirname(file), { recursive: true });
|
||||
await writeFile(file, source, 'utf8');
|
||||
}
|
||||
|
||||
function scan(root) {
|
||||
return scanImportBoundaries({
|
||||
frameworkDir: join(root, 'framework'),
|
||||
gamesDir: join(root, 'games'),
|
||||
});
|
||||
}
|
||||
|
||||
test('scanner rejects sdk contracts importing a framework platform path', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import '../platform/session.ts'\n");
|
||||
assert.match(scan(root)[0].message, /sdk.*platform/);
|
||||
});
|
||||
|
||||
test('scanner rejects game imports of framework net internals', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'games/a/assets/game/bad.ts', "import '../../../framework/net/net-client.ts'\n");
|
||||
assert.match(scan(root)[0].message, /game.*framework\/net/);
|
||||
});
|
||||
|
||||
test('scanner permits a game import of the public sdk barrel', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'games/a/assets/game/allowed.ts', "import '../../../../framework/sdk/index.ts'\n");
|
||||
assert.deepEqual(scan(root), []);
|
||||
});
|
||||
|
||||
test('scanner rejects games importing migration-only sdk declarations', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'games/a/assets/game/bad.ts', "import type { GameContext } from '../../../../framework/sdk/index.ts'\n");
|
||||
assert.match(scan(root)[0].message, /game.*GameContext.*migration-only/);
|
||||
});
|
||||
|
||||
test('scanner rejects cc imports in sdk contracts', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import { Node } from 'cc'\n");
|
||||
assert.match(scan(root)[0].message, /sdk\/contracts.*cc/);
|
||||
});
|
||||
|
||||
test('scanner permits cc imports in game code', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'games/a/assets/game/allowed.ts', "import { Node } from 'cc'\n");
|
||||
assert.deepEqual(scan(root), []);
|
||||
});
|
||||
|
||||
test('scanner rejects framework imports resolving under games', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'framework/ui/bad.ts', "import '../../games/a/assets/game/entry.ts'\n");
|
||||
assert.match(scan(root)[0].message, /framework.*games/);
|
||||
});
|
||||
|
||||
test('scanner rejects new production imports of a quarantined legacy runtime', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'framework/application/bad.ts', "import '../net/net-client.ts'\n");
|
||||
const violations = scan(root);
|
||||
assert.equal(violations.length, 1);
|
||||
assert.match(violations[0].message, /new production code.*legacy runtime/);
|
||||
});
|
||||
|
||||
test('scanner rejects string-literal dynamic imports that cross a boundary', async () => {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, 'games/a/assets/game/bad.ts', "await import('../../../framework/net/net-client.ts')\n");
|
||||
assert.match(scan(root)[0].message, /game.*framework\/net/);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import type {
|
||||
GameEntry, GameHost, GameModule, GameServerMessage,
|
||||
PlatformGameSnapshot, PlatformToGameEvent,
|
||||
} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts';
|
||||
|
||||
test('single GameEntry contract has no registry or runtime version member', () => {
|
||||
const module: GameModule = {
|
||||
attach(_host: GameHost) {},
|
||||
handlePlatformEvent(_event: PlatformToGameEvent) {},
|
||||
handleGameMessage(_message: GameServerMessage) {},
|
||||
restore(_deskinfo: unknown) {},
|
||||
dispose() {},
|
||||
};
|
||||
const entry: GameEntry = {
|
||||
key: 'fixture', gameId: 41, route: 'fixture-route',
|
||||
resolveSeatCount: () => 4,
|
||||
createModule: () => module,
|
||||
};
|
||||
assert.equal(entry.route, 'fixture-route');
|
||||
assert.equal('apiVersion' in entry, false);
|
||||
});
|
||||
|
||||
test('public snapshot is plain readonly data', () => {
|
||||
const value = {} as PlatformGameSnapshot;
|
||||
assert.equal('state' in value, false);
|
||||
});
|
||||
Reference in New Issue
Block a user