Files
youle_cocos/cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs
T

633 lines
26 KiB
JavaScript

import { afterEach, test } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, readdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
findCompatibilityDependency,
LEGACY_RUNTIME,
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, tsconfigPath) {
return scanImportBoundaries({
frameworkDir: join(root, 'framework'),
gamesDir: join(root, 'games'),
tsconfigPath: tsconfigPath ? join(root, tsconfigPath) : undefined,
});
}
const frameworkRoot = fileURLToPath(new URL('../../YouleNexus/assets/framework/', import.meta.url));
const legacyRuntimeFiles = new Set([
'net/net-client.ts',
'platform/session.ts',
'platform/startup.ts',
'platform/room-rpc-bus.ts',
'platform/readonly.ts',
'platform/stores/app-store.ts',
'platform/stores/player-store.ts',
'platform/stores/room-store.ts',
'platform/stores/types.ts',
'protocol/room-handlers.ts',
].map((file) => resolve(frameworkRoot, file)));
function scanCompatibilityFixture(root, entry = 'framework/protocol/router.ts', tsconfigPath) {
return findCompatibilityDependency({
entry: join(root, entry),
forbiddenFiles: [join(root, 'framework/platform/session.ts')],
traversalRoot: join(root, 'framework'),
tsconfigPath: tsconfigPath ? join(root, tsconfigPath) : undefined,
});
}
test('every modern framework file is transitively isolated from the exact legacy compatibility island', () => {
const files = readdirSync(frameworkRoot, { recursive: true })
.filter(file => file.endsWith('.ts'))
.map(file => resolve(frameworkRoot, file));
assert.deepEqual(new Set(files.filter(file => LEGACY_RUNTIME.test(file))), legacyRuntimeFiles);
const entries = files.filter(file => !legacyRuntimeFiles.has(file));
assert.ok(existsSync(resolve(frameworkRoot, 'platform/runtime.ts')));
assert.ok(entries.includes(resolve(frameworkRoot, 'sdk/index.ts')));
for (const entry of entries) {
const dependency = findCompatibilityDependency({
entry,
forbiddenFiles: [...legacyRuntimeFiles],
traversalRoot: frameworkRoot,
tsconfigPath: fileURLToPath(new URL('../../tsconfig.framework.json', import.meta.url)),
});
assert.equal(
dependency,
null,
dependency?.path.map((file) => file.slice(frameworkRoot.length)).join(' -> '),
);
}
});
test('obsolete SDK identifier inventory stays within explicit negative tests and one legacy comment', () => {
const result = spawnSync('rg', [
'-l', '--glob', '*.ts', '--glob', '*.mjs',
'\\b(GameContext|IGameModule|ActiveGame)\\b',
'YouleNexus/assets/framework', 'framework-tests',
], { cwd: fileURLToPath(new URL('../../', import.meta.url)), encoding: 'utf8' });
assert.ifError(result.error);
assert.ok(result.status === 0 || result.status === 1, result.stderr);
const allowlist = new Set([
'framework-tests/architecture/import-boundaries.test.mjs',
'YouleNexus/assets/framework/protocol/room-handlers.ts',
]);
const matches = result.stdout.trim().split(/\r?\n/).filter(Boolean).map(file => file.replaceAll('\\', '/'));
assert.deepEqual(matches.filter(file => !allowlist.has(file)), []);
});
test('compatibility graph catches static imports, re-exports, and import types through barrels', async () => {
const cases = [
"import '../platform/session.ts'\n",
"import Legacy = require('../platform/session.ts')\n",
"export * from '../platform/session.ts'\n",
"export type { LegacySession } from '../platform/session.ts'\n",
"type Legacy = import('../platform/session.ts').LegacySession\n",
];
for (const source of cases) {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/platform/session.ts', 'export interface LegacySession {}\n');
await writeFixture(root, 'framework/protocol/barrel.ts', source);
await writeFixture(root, 'framework/protocol/router.ts', "export * from './barrel.ts'\n");
const found = scanCompatibilityFixture(root);
assert.equal(found?.kind, 'forbidden', source);
assert.deepEqual(
found?.path.map((file) => file.replaceAll('\\', '/').split('/framework/')[1]),
['protocol/router.ts', 'protocol/barrel.ts', 'platform/session.ts'],
source,
);
}
});
test('compatibility graph reports direct and ordinary transitive dependency paths', async () => {
const directRoot = await createFixtureRoot();
await writeFixture(directRoot, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(directRoot, 'framework/protocol/router.ts', "import '../platform/session.ts'\n");
assert.equal(scanCompatibilityFixture(directRoot)?.path.length, 2);
const transitiveRoot = await createFixtureRoot();
await writeFixture(transitiveRoot, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(transitiveRoot, 'framework/protocol/helper.ts', "import '../platform/session.ts'\n");
await writeFixture(transitiveRoot, 'framework/protocol/router.ts', "import './helper.ts'\n");
assert.equal(scanCompatibilityFixture(transitiveRoot)?.path.length, 3);
});
test('compatibility graph catches string and no-substitution-template dynamic imports', async () => {
for (const source of [
"await import('../platform/session.ts')\n",
'await import(`../platform/session.ts`)\n',
]) {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(root, 'framework/protocol/router.ts', source);
assert.equal(scanCompatibilityFixture(root)?.kind, 'forbidden', source);
}
});
test('compatibility graph treats literal require calls as static dependencies', async () => {
for (const source of [
"require('../platform/session.ts')\n",
'require(`../platform/session.ts`)\n',
]) {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(root, 'framework/protocol/router.ts', source);
const found = scanCompatibilityFixture(root);
assert.equal(found?.kind, 'forbidden', source);
assert.deepEqual(
found?.path.map((file) => file.replaceAll('\\', '/').split('/framework/')[1]),
['protocol/router.ts', 'platform/session.ts'],
source,
);
}
});
test('compatibility graph resolves tsconfig path aliases to canonical files', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(root, 'framework/protocol/router.ts', "import '@legacy/session'\n");
await writeFixture(root, 'tsconfig.json', JSON.stringify({
compilerOptions: {
baseUrl: '.',
paths: { '@legacy/*': ['framework/platform/*'] },
module: 'ESNext',
moduleResolution: 'Bundler',
allowImportingTsExtensions: true,
},
}));
const found = scanCompatibilityFixture(root, undefined, 'tsconfig.json');
assert.equal(found?.kind, 'forbidden');
assert.ok(found?.path.at(-1)?.replaceAll('\\', '/').endsWith('/framework/platform/session.ts'));
});
test('compatibility graph fails closed on a reachable non-static dynamic import', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(
root,
'framework/protocol/router.ts',
"const target = '../platform/session.ts'; void import(target)\n",
);
const found = scanCompatibilityFixture(root);
assert.equal(found?.kind, 'dynamic');
assert.ok(found?.path[0]?.replaceAll('\\', '/').endsWith('/framework/protocol/router.ts'));
});
test('compatibility graph fails closed on reachable non-static require calls', async () => {
for (const source of [
"const target = '../platform/session.ts'; require(target)\n",
"const name = 'session'; require(`../platform/${name}.ts`)\n",
]) {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(root, 'framework/protocol/router.ts', source);
const found = scanCompatibilityFixture(root);
assert.equal(found?.kind, 'dynamic', source);
assert.ok(
found?.path[0]?.replaceAll('\\', '/').endsWith('/framework/protocol/router.ts'),
source,
);
}
});
test('compatibility graph applies the same isolation to a future platform/runtime.ts entry', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n');
await writeFixture(root, 'framework/application/barrel.ts', "export * from '../platform/session.ts'\n");
await writeFixture(root, 'framework/platform/runtime.ts', "import '../application/barrel.ts'\n");
const found = scanCompatibilityFixture(root, 'framework/platform/runtime.ts');
assert.equal(found?.kind, 'forbidden');
assert.equal(found?.path.length, 3);
});
test('platform/runtime.ts cannot import any former startup owner directly', async () => {
const cases = [
{ file: 'framework/platform/startup.ts', specifier: './startup.ts' },
{ file: 'framework/net/net-client.ts', specifier: '../net/net-client.ts' },
{ file: 'framework/platform/session.ts', specifier: './session.ts' },
{ file: 'framework/platform/room-rpc-bus.ts', specifier: './room-rpc-bus.ts' },
];
for (const item of cases) {
const root = await createFixtureRoot();
await writeFixture(root, item.file, 'export const legacy = true\n');
await writeFixture(
root,
'framework/platform/runtime.ts',
`import '${item.specifier}'\n`,
);
const found = findCompatibilityDependency({
entry: join(root, 'framework/platform/runtime.ts'),
forbiddenFiles: [join(root, item.file)],
traversalRoot: join(root, 'framework'),
});
assert.equal(found?.kind, 'forbidden', item.file);
assert.deepEqual(
found?.path.map((file) => file.replaceAll('\\', '/').split('/framework/')[1]),
['platform/runtime.ts', item.file.slice('framework/'.length)],
item.file,
);
}
});
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 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 resolves and rejects an sdk runtime path alias into framework implementation', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/core/internal.ts', 'export const value = true\n');
await writeFixture(
root,
'framework/sdk/runtime/bad.ts',
"import { value } from '@framework/core/internal'; void value\n",
);
await writeFixture(root, 'tsconfig.json', JSON.stringify({
compilerOptions: {
baseUrl: '.',
paths: { '@framework/*': ['framework/*'] },
module: 'ESNext',
moduleResolution: 'Bundler',
allowImportingTsExtensions: true,
},
}));
const violations = scan(root, 'tsconfig.json');
assert.equal(violations.length, 1);
assert.equal(violations[0].specifier, '@framework/core/internal');
assert.match(violations[0].message, /sdk.*framework implementation.*framework\/core/);
});
test('scanner resolves and rejects a nested sdk CommonJS path alias into framework implementation', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/core/internal.ts', 'export const value = true\n');
await writeFixture(
root,
'framework/sdk/runtime/nested/bad.ts',
"const value = require('@framework/core/internal'); void value\n",
);
await writeFixture(root, 'tsconfig.json', JSON.stringify({
compilerOptions: {
baseUrl: '.',
paths: { '@framework/*': ['framework/*'] },
module: 'ESNext',
moduleResolution: 'Bundler',
allowImportingTsExtensions: true,
},
}));
const violations = scan(root, 'tsconfig.json');
assert.equal(violations.length, 1);
assert.equal(violations[0].specifier, '@framework/core/internal');
assert.match(violations[0].message, /sdk.*framework implementation.*framework\/core/);
});
test('scanner resolves and rejects a TypeScript import-equals sdk path alias', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/core/internal.ts', 'export const value = true\n');
await writeFixture(
root,
'framework/sdk/runtime/nested/bad.ts',
"import alias = require('@framework/core/internal'); void alias\n",
);
await writeFixture(root, 'tsconfig.json', JSON.stringify({
compilerOptions: {
baseUrl: '.',
paths: { '@framework/*': ['framework/*'] },
module: 'ESNext',
moduleResolution: 'Bundler',
allowImportingTsExtensions: true,
},
}));
const violations = scan(root, 'tsconfig.json');
assert.equal(violations.length, 1);
assert.equal(violations[0].specifier, '@framework/core/internal');
assert.match(violations[0].message, /sdk.*framework implementation.*framework\/core/);
});
test('scanner resolves and rejects a no-substitution-template dynamic sdk path alias', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/core/internal.ts', 'export const value = true\n');
await writeFixture(
root,
'framework/sdk/runtime/nested/bad.ts',
'void import(`@framework/core/internal`)\n',
);
await writeFixture(root, 'tsconfig.json', JSON.stringify({
compilerOptions: {
baseUrl: '.',
paths: { '@framework/*': ['framework/*'] },
module: 'ESNext',
moduleResolution: 'Bundler',
allowImportingTsExtensions: true,
},
}));
const violations = scan(root, 'tsconfig.json');
assert.equal(violations.length, 1);
assert.equal(violations[0].specifier, '@framework/core/internal');
assert.match(violations[0].message, /sdk.*framework implementation.*framework\/core/);
});
test('scanner rejects Cocos imports throughout sdk runtime', async () => {
for (const specifier of ['cc', 'cc/env']) {
const root = await createFixtureRoot();
await writeFixture(
root,
'framework/sdk/runtime/bad.ts',
`import { Node } from '${specifier}'; void Node\n`,
);
const violations = scan(root);
assert.equal(violations.length, 1, specifier);
assert.equal(violations[0].specifier, specifier);
assert.match(violations[0].message, /sdk.*Cocos/);
}
});
test('scanner rejects a no-substitution-template dynamic Cocos import in nested sdk code', async () => {
const root = await createFixtureRoot();
await writeFixture(
root,
'framework/sdk/testing/nested/bad.ts',
'void import(`cc/env`)\n',
);
const violations = scan(root);
assert.equal(violations.length, 1);
assert.equal(violations[0].specifier, 'cc/env');
assert.match(violations[0].message, /sdk.*Cocos/);
});
test('scanner rejects a CommonJS Cocos import in nested sdk code', async () => {
const root = await createFixtureRoot();
await writeFixture(
root,
'framework/sdk/testing/nested/bad.ts',
"const env = require('cc/env'); void env\n",
);
const violations = scan(root);
assert.equal(violations.length, 1);
assert.equal(violations[0].specifier, 'cc/env');
assert.match(violations[0].message, /sdk.*Cocos/);
});
test('sdk public barrel has no exemptions for any former migration dependency', async () => {
const root = await createFixtureRoot();
for (const file of [
'framework/core/events.ts',
'framework/core/reactive.ts',
'framework/platform/readonly.ts',
'framework/platform/stores/types.ts',
'framework/core/constants.ts',
]) {
await writeFixture(root, file, 'export interface Value {}\n');
}
await writeFixture(root, 'framework/sdk/index.ts', [
"import type { Value as Events } from '../core/events.ts'; void (0 as unknown as Events);",
"import type { Value as Reactive } from '../core/reactive.ts'; void (0 as unknown as Reactive);",
"import type { Value as ReadonlyStore } from '../platform/readonly.ts'; void (0 as unknown as ReadonlyStore);",
"import type { Value as StoreTypes } from '../platform/stores/types.ts'; void (0 as unknown as StoreTypes);",
"import type { Value as Constants } from '../core/constants.ts'; void (0 as unknown as Constants);",
].join('\n'));
const violations = scan(root);
assert.equal(violations.length, 5);
for (const violation of violations) assert.match(violation.message, /sdk.*framework implementation/);
});
test('former active-game file has no exemption from obsolete SDK name checks', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/protocol/active-game.ts', "import type { IGameModule } from '../sdk/index.ts'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /IGameModule.*migration-only/);
});
test('obsolete SDK names are rejected in destructured CommonJS and dynamic imports and named re-exports', async () => {
for (const source of [
"const { GameContext: Context } = require('../sdk/index.ts');",
"const { IGameModule } = require(`../sdk/index.ts`);",
"const { GameContext } = await import('../sdk/index.ts');",
"const Context = require('../sdk/index.ts').GameContext;",
"export type { IGameModule } from '../sdk/index.ts';",
"type Context = import('../sdk/index.ts').GameContext;",
]) {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/application/bad.ts', source);
const violations = scan(root);
assert.equal(violations.length, 1, source);
assert.match(violations[0].message, /migration-only/, source);
}
});
test('scanner permits sdk-internal relatives and ordinary external pure dependencies', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/sdk/contracts/index.ts', 'export interface Contract {}\n');
await writeFixture(root, 'framework/sdk/runtime/helper.ts', 'export const helper = true\n');
await writeFixture(root, 'framework/sdk/runtime/allowed.ts', [
"import type { Contract } from '../contracts/index.ts'; void (0 as unknown as Contract);",
"import { helper } from './helper.ts'; void helper;",
"import type { PureValue } from 'pure-external'; void (0 as unknown as PureValue);",
"const pure = require('pure-commonjs-external'); void pure;",
].join('\n'));
assert.deepEqual(scan(root), []);
});
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 rejects multiline game imports of framework net internals', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'games/a/assets/game/bad.ts', "import {\n NetClient,\n} from '../../../../framework/net/net-client.ts'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /game.*framework\/net/);
});
test('scanner rejects multiline sdk contract imports outside their sibling directory', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import {\n PlatformSession,\n} from '../../platform/session.ts'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/);
});
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 rejects non-relative imports in sdk contracts', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import 'node:fs'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /sdk\/contracts.*non-relative/);
});
test('scanner rejects import-type leaks from sdk contracts', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/sdk/contracts/bad.ts', "type ReadonlyRoom = import('../../platform/readonly.ts').ReadonlyRoomStore\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/);
});
test('scanner rejects export-star leaks from sdk contracts', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/sdk/contracts/index.ts', "export * from '../../platform/readonly.ts'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/);
});
test('scanner rejects named re-export leaks from sdk contracts', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/sdk/contracts/index.ts', "export { ReadonlyRoomStore } from '../../platform/readonly.ts'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/);
});
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 framework production imports of migration-only sdk declarations', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/application/bad.ts', "import type { GameContext } from '../sdk/index.ts'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /GameContext.*migration-only/);
});
test('scanner rejects alias imports of migration-only sdk declarations', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/application/bad.ts', "import type { IGameModule } from '@internal/sdk'\n");
const violations = scan(root);
assert.equal(violations.length, 1);
assert.match(violations[0].message, /IGameModule.*migration-only/);
});
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 CommonJS imports of a quarantined legacy runtime', async () => {
const root = await createFixtureRoot();
await writeFixture(root, 'framework/net/net-client.ts', 'export const legacy = true\n');
await writeFixture(
root,
'framework/application/nested/bad.ts',
"const legacy = require('../../net/net-client.ts'); void legacy\n",
);
const violations = scan(root);
assert.equal(violations.length, 1);
assert.equal(violations[0].specifier, '../../net/net-client.ts');
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/);
});