fix(sdk): enforce runtime dependency boundary
This commit is contained in:
@@ -5,9 +5,8 @@ import type {
|
||||
GameServerMessage,
|
||||
PlatformToGameEvent,
|
||||
} from '../contracts/index.ts';
|
||||
import { Route } from '../../core/constants.ts';
|
||||
|
||||
const RESERVED_ROUTES = new Set<string>(Object.values(Route));
|
||||
const RESERVED_GAME_ENTRY_ROUTES = new Set(['platform', 'agent', 'room']);
|
||||
const GAME_SESSION_OPEN_FAULT_BRAND = Symbol.for(
|
||||
'youle.framework.sdk.GameSessionOpenFault',
|
||||
);
|
||||
@@ -104,7 +103,7 @@ export function assertGameEntry(entry: GameEntry): void {
|
||||
/** The single GameEntry route admission check shared by runtime adapters. */
|
||||
export function assertGameRoute(route: unknown): asserts route is string {
|
||||
assertNonEmptyText(route, 'route');
|
||||
if (RESERVED_ROUTES.has(route)) {
|
||||
if (RESERVED_GAME_ENTRY_ROUTES.has(route)) {
|
||||
throw new Error(`GameEntry reserved route: ${route}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,36 @@ test('scanner rejects sdk contracts importing a framework platform path', async
|
||||
assert.match(scan(root)[0].message, /sdk.*platform/);
|
||||
});
|
||||
|
||||
test('scanner rejects every sdk runtime dependency on framework implementation layers', async () => {
|
||||
const layers = [
|
||||
'core',
|
||||
'platform',
|
||||
'net',
|
||||
'config',
|
||||
'native',
|
||||
'protocol',
|
||||
'application',
|
||||
'domain',
|
||||
'presentation',
|
||||
'ui',
|
||||
'compat',
|
||||
];
|
||||
|
||||
for (const layer of layers) {
|
||||
const root = await createFixtureRoot();
|
||||
await writeFixture(root, `framework/${layer}/internal.ts`, 'export const value = true\n');
|
||||
await writeFixture(
|
||||
root,
|
||||
'framework/sdk/runtime/bad.ts',
|
||||
`import { value } from '../../${layer}/internal.ts'; void value\n`,
|
||||
);
|
||||
|
||||
const violations = scan(root);
|
||||
assert.equal(violations.length, 1, layer);
|
||||
assert.match(violations[0].message, /sdk.*framework implementation/, layer);
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
@@ -122,14 +122,6 @@ test('Task 1 route validator is the shared authority for game-route admission',
|
||||
for (const route of ['platform', 'agent', 'room']) {
|
||||
assert.throws(() => assertGameRoute(route), /reserved.*route/i);
|
||||
}
|
||||
|
||||
const validatorSource = readFileSync(new URL(
|
||||
'../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts',
|
||||
import.meta.url,
|
||||
), 'utf8');
|
||||
assert.match(validatorSource, /import \{ Route \} from '\.\.\/\.\.\/core\/constants\.ts';/);
|
||||
assert.match(validatorSource, /new Set<string>\(Object\.values\(Route\)\)/);
|
||||
assert.doesNotMatch(validatorSource, /new Set\(\[['"]platform['"]/);
|
||||
});
|
||||
|
||||
test('execute exposes only the GameHost command union and delegates semantic commands', () => {
|
||||
|
||||
@@ -221,6 +221,7 @@ function findViolation(context) {
|
||||
const filePath = displayPath(file);
|
||||
const resolvedPath = resolved ? displayPath(resolved) : specifier;
|
||||
const isContract = CONTRACTS_PATH.test(filePath);
|
||||
const isSdk = SDK_ALLOWED.test(filePath);
|
||||
const isGame = isInside(file, gamesDir);
|
||||
const isFramework = isInside(file, frameworkDir);
|
||||
|
||||
@@ -232,6 +233,14 @@ function findViolation(context) {
|
||||
return violation(filePath, specifier, `sdk/contracts may import only sibling contracts; sdk path resolved to ${resolvedPath}`);
|
||||
}
|
||||
|
||||
if (isSdk
|
||||
&& !isSdkMigrationBarrel(filePath, frameworkDir)
|
||||
&& resolved
|
||||
&& isInside(resolved, frameworkDir)
|
||||
&& !SDK_ALLOWED.test(resolvedPath)) {
|
||||
return violation(filePath, specifier, `sdk cannot import framework implementation ${resolvedPath}`);
|
||||
}
|
||||
|
||||
if (isGame && resolved && isFrameworkReference(resolved, frameworkDir) && !SDK_ALLOWED.test(resolvedPath)) {
|
||||
return violation(filePath, specifier, `game code may import only framework/sdk; game import resolved to ${resolvedPath}`);
|
||||
}
|
||||
@@ -264,6 +273,10 @@ function isLegacyRuntimeImporter(filePath, frameworkDir) {
|
||||
return filePath === `${normalizedFrameworkDir}/sdk/index.ts` || LEGACY_RUNTIME.test(filePath);
|
||||
}
|
||||
|
||||
function isSdkMigrationBarrel(filePath, frameworkDir) {
|
||||
return filePath === `${displayPath(frameworkDir)}/sdk/index.ts`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ActiveGame is the retained compatibility owner of IGameModule until Task 11
|
||||
* removes both it and the migration-only declarations from sdk/index.ts.
|
||||
|
||||
Reference in New Issue
Block a user