071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import { readdir } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
import { ESLint } from 'eslint';
|
|
import type { Plugin } from 'vite';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import viteConfig, {
|
|
isForbiddenPublicAssetPath,
|
|
isRetiredApiPath,
|
|
isRetiredFrontendModuleId,
|
|
} from '../vite.config';
|
|
|
|
async function resolveVitePlugin(name: string) {
|
|
const resolvedConfig =
|
|
typeof viteConfig === 'function'
|
|
? await viteConfig({ command: 'serve', mode: 'test' })
|
|
: viteConfig;
|
|
const plugins = (resolvedConfig.plugins ?? []).flat(Infinity) as Plugin[];
|
|
return plugins.find((plugin) => plugin?.name === name);
|
|
}
|
|
|
|
function resolveRetiredCssPlugin() {
|
|
return resolveVitePlugin('retired-creation-template-css');
|
|
}
|
|
|
|
function workspacePath(relativePath: string) {
|
|
return path.resolve(process.cwd(), relativePath).replaceAll('\\', '/');
|
|
}
|
|
|
|
async function readTypeScriptSourceFiles(directory: string): Promise<string[]> {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const files = await Promise.all(
|
|
entries.map(async (entry) => {
|
|
const entryPath = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
return readTypeScriptSourceFiles(entryPath);
|
|
}
|
|
if (
|
|
!entry.isFile() ||
|
|
!/\.(?:ts|tsx)$/u.test(entry.name) ||
|
|
entry.name.endsWith('.d.ts')
|
|
) {
|
|
return [];
|
|
}
|
|
return [entryPath.replaceAll('\\', '/')];
|
|
}),
|
|
);
|
|
return files.flat();
|
|
}
|
|
|
|
describe('retired creation template CSS plugin', () => {
|
|
it('runs before Tailwind turns source CSS into a Vite JavaScript module', async () => {
|
|
const plugin = await resolveRetiredCssPlugin();
|
|
|
|
expect(plugin).toBeDefined();
|
|
expect(plugin?.enforce).toBe('pre');
|
|
});
|
|
|
|
it('keeps the active creation landing CSS while removing retired template CSS', async () => {
|
|
const plugin = await resolveRetiredCssPlugin();
|
|
const transform = plugin?.transform;
|
|
|
|
expect(typeof transform).toBe('function');
|
|
if (typeof transform !== 'function') {
|
|
return;
|
|
}
|
|
|
|
const result = await transform.call(
|
|
{} as never,
|
|
"@import 'tailwindcss';\n.creation-landing { color: red; }\n.puzzle-runtime { color: blue; }\n.pixel-modal-shell { color: black; }\n@font-face { font-family: 'Fusion Pixel'; src: url('/fusion-pixel.ttf'); }",
|
|
'/workspace/src/index.css',
|
|
);
|
|
const code = typeof result === 'string' ? result : result?.code;
|
|
|
|
expect(code).toContain('.creation-landing');
|
|
expect(code).not.toContain('.puzzle-runtime');
|
|
expect(code).not.toContain('.pixel-modal-shell');
|
|
expect(code).not.toContain('fusion-pixel.ttf');
|
|
expect(code).toContain('@source "./components/creation-home"');
|
|
});
|
|
});
|
|
|
|
describe('retired creation template module boundary plugin', () => {
|
|
it('rejects retired component and service modules from the active Vite graph', async () => {
|
|
const plugin = await resolveVitePlugin('retired-creation-template-modules');
|
|
const transform = plugin?.transform;
|
|
|
|
expect(plugin?.enforce).toBe('pre');
|
|
expect(typeof transform).toBe('function');
|
|
if (typeof transform !== 'function') {
|
|
return;
|
|
}
|
|
|
|
expect(() =>
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath('src/components/rpg-entry/RpgEntryHomeView.tsx'),
|
|
),
|
|
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
|
expect(() =>
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath('src/services/runtimeRequest.ts'),
|
|
),
|
|
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
|
expect(() =>
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath(
|
|
'src/services/input-devices/runtimeDragInputController.ts',
|
|
),
|
|
),
|
|
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
|
expect(() =>
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath('src/services/runtimeAudioFeedback.ts'),
|
|
),
|
|
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
|
expect(() =>
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath('src/types/game.ts'),
|
|
),
|
|
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
|
expect(() =>
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
`${workspacePath('src/services/rpg-entry/rpgProfileClient.ts')}?t=1`,
|
|
),
|
|
).toThrow(/退役创作模板模块不得进入现役 Vite 依赖图/u);
|
|
});
|
|
|
|
it('rejects retired top-level apps, data, games, prompts, routes, and services', () => {
|
|
for (const path of [
|
|
'src/App.test.tsx',
|
|
'src/App.tsx',
|
|
'src/RpgRuntimeApp.tsx',
|
|
'src/Match3DPlaygroundApp.tsx',
|
|
'src/components/CustomWorldGenerationView.tsx',
|
|
'src/components/InventoryPanel.tsx',
|
|
'src/components/common/PublishShareModal.tsx',
|
|
'src/components/platform-entry/PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx',
|
|
'src/components/platform-entry/platformMatch3DRuntimeProfile.ts',
|
|
'src/data/customWorldLibrary.ts',
|
|
'src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx',
|
|
'src/hooks/combat/battlePlan.ts',
|
|
'src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts',
|
|
'src/hooks/rpg-session/useRpgRuntimeSession.ts',
|
|
'src/hooks/useNpcInteractionFlow.ts',
|
|
'src/persistence/runtimeSnapshot.ts',
|
|
'src/prompts/customWorldPrompts.ts',
|
|
'src/routing/appRoutes.tsx',
|
|
'src/routing/runtimeNotFoundRecovery.ts',
|
|
'src/services/ai.ts',
|
|
'src/services/puzzleReferenceImage.ts',
|
|
]) {
|
|
expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('allows active creation, project, and profile modules', async () => {
|
|
const plugin = await resolveVitePlugin('retired-creation-template-modules');
|
|
const transform = plugin?.transform;
|
|
|
|
expect(typeof transform).toBe('function');
|
|
if (typeof transform !== 'function') {
|
|
return;
|
|
}
|
|
|
|
expect(
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath('src/components/creation-home/CreationLandingView.tsx'),
|
|
),
|
|
).toBeNull();
|
|
expect(
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath(
|
|
'src/components/platform-entry/PlatformActiveProfileView.tsx',
|
|
),
|
|
),
|
|
).toBeNull();
|
|
expect(
|
|
transform.call(
|
|
{} as never,
|
|
'export {}',
|
|
workspacePath('src/services/platform-entry/platformProfileClient.ts'),
|
|
),
|
|
).toBeNull();
|
|
expect(isRetiredFrontendModuleId(workspacePath('src/ActiveApp.tsx'))).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
isRetiredFrontendModuleId(
|
|
workspacePath('src/services/image-editor/editorProjectClient.ts'),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
isRetiredFrontendModuleId(
|
|
workspacePath('src/components/ResolvedAssetAudio.tsx'),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
isRetiredFrontendModuleId(
|
|
workspacePath('src/components/ResolvedAssetImage.tsx'),
|
|
),
|
|
).toBe(false);
|
|
for (const path of [
|
|
'src/hooks/useGameSettings.ts',
|
|
'src/persistence/storage.ts',
|
|
'src/routing/activeAppRoutes.tsx',
|
|
'src/services/apiClient.ts',
|
|
]) {
|
|
expect(isRetiredFrontendModuleId(workspacePath(path))).toBe(false);
|
|
}
|
|
expect(
|
|
isRetiredFrontendModuleId(
|
|
workspacePath(
|
|
'packages/shared/src/components/PlatformMudPointWalletEntry.tsx',
|
|
),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('retired frontend ESLint boundary', () => {
|
|
it('keeps every ESLint-excluded source module outside the Vite graph', async () => {
|
|
const eslint = new ESLint({ cwd: process.cwd() });
|
|
const sourceFiles = await readTypeScriptSourceFiles('src');
|
|
const retiredSourceFiles: string[] = [];
|
|
for (const sourceFile of sourceFiles) {
|
|
if (await eslint.isPathIgnored(sourceFile)) {
|
|
retiredSourceFiles.push(sourceFile);
|
|
}
|
|
}
|
|
|
|
expect(retiredSourceFiles.length).toBeGreaterThan(0);
|
|
expect(
|
|
retiredSourceFiles.filter(
|
|
(sourceFile) => !isRetiredFrontendModuleId(workspacePath(sourceFile)),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('ignores retired root modules while keeping active root modules lintable', async () => {
|
|
const eslint = new ESLint({ cwd: process.cwd() });
|
|
for (const path of [
|
|
'src/components/CustomWorldGenerationView.tsx',
|
|
'src/hooks/useNpcInteractionFlow.ts',
|
|
'src/persistence/runtimeSnapshot.ts',
|
|
'src/routing/runtimeNotFoundRecovery.ts',
|
|
'src/services/puzzleReferenceImage.ts',
|
|
]) {
|
|
await expect(eslint.isPathIgnored(path)).resolves.toBe(true);
|
|
}
|
|
for (const path of [
|
|
'src/components/ResolvedAssetAudio.tsx',
|
|
'src/components/ResolvedAssetImage.tsx',
|
|
'src/hooks/useGameSettings.ts',
|
|
'src/persistence/storage.ts',
|
|
'src/routing/activeAppRoutes.tsx',
|
|
'src/services/apiClient.ts',
|
|
]) {
|
|
await expect(eslint.isPathIgnored(path)).resolves.toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('retired creation template API boundary', () => {
|
|
it('returns a dev-server 404 boundary for retired API prefixes only', () => {
|
|
expect(isRetiredApiPath('/api/creation-entry/config')).toBe(true);
|
|
expect(isRetiredApiPath('/api/creation/puzzle/sessions')).toBe(true);
|
|
expect(isRetiredApiPath('/api/public-works/PZ-12345678')).toBe(true);
|
|
expect(isRetiredApiPath('/api/runtime/settings')).toBe(false);
|
|
expect(isRetiredApiPath('/api/editor/showcase/resources')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('retired creation template asset boundary', () => {
|
|
it('returns a dev-server 404 boundary for retired generated asset proxies', () => {
|
|
for (const pathname of [
|
|
'/generated-character-drafts/hero/visual/candidate.png',
|
|
'/generated-characters/hero/visual/master.png',
|
|
'/generated-animations/hero/idle/frame01.png',
|
|
'/generated-big-fish-assets/session-1/level/image.png',
|
|
'/generated-puzzle-assets/session-1/candidate/image.png',
|
|
'/generated-custom-world-scenes/world-1/camp/scene.png',
|
|
'/generated-custom-world-covers/world-1/cover.webp',
|
|
'/generated-bark-battle-assets/draft/player/image.webp',
|
|
'/generated-qwen-sprites/master/candidate-01.png',
|
|
]) {
|
|
expect(isForbiddenPublicAssetPath(pathname)).toBe(true);
|
|
}
|
|
expect(
|
|
isForbiddenPublicAssetPath('/generated-editor-images/asset.png'),
|
|
).toBe(true);
|
|
expect(isForbiddenPublicAssetPath('/creation-home/logo.png')).toBe(false);
|
|
expect(
|
|
isForbiddenPublicAssetPath(
|
|
'/branding/mobile-home-welcome-taonier-ip.png',
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|