6k6g-v3 / src /lib /code-generator.test.ts
fanyubo
fix: ไฟฎๅค TypeScript ๆž„ๅปบ้”™่ฏฏ + ๆทปๅŠ ็ผบๅคฑไพ่ต–
2aec246
Raw
History Blame Contribute Delete
9.43 kB
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { parseCodeOutput, generateGameCode } from './code-generator';
import type { GeneratedCodeFile, GenerateCodeParams } from './code-generator';
import type { GameDesignDocument } from './gdd-generator';
import type { AssetPack } from './asset-generator';
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
// Mock callLLM
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
vi.mock('./llm-proxy', async (importOriginal) => {
const actual = await importOriginal<typeof import('./llm-proxy')>();
return {
...actual,
callLLM: vi.fn(),
};
});
import { callLLM } from './llm-proxy';
beforeEach(() => {
vi.mocked(callLLM).mockClear();
});
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
// ๆต‹่ฏ•ๆ•ฐๆฎ
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
const MOCK_FILES: GeneratedCodeFile[] = [
{ path: 'src/main.ts', content: "import Phaser from 'phaser';\nconsole.log('hello');" },
{ path: 'src/scenes/MainScene.ts', content: "export class MainScene {}" },
];
const MOCK_JSON = JSON.stringify(MOCK_FILES);
const VALID_GDD: GameDesignDocument = {
architecture: { sceneKeys: ['MainScene'], levelManagerConfig: 'linear', mainEntryPoints: ['src/main.ts'] },
assetRegistry: [],
gameConfig: {},
entities: [],
levels: [],
roadmap: [],
metadata: { title: 'Test', tagline: '', archetype: 'platformer', generatedAt: '2026-01-01' },
};
const EMPTY_ASSET_PACK: AssetPack = {};
const TEMPLATE_FILES = [
{ path: 'platformer/src/main.ts', content: 'import Phaser from "phaser";' },
];
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
// parseCodeOutput ๆต‹่ฏ•
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
describe('parseCodeOutput', () => {
it('ๅบ”ๆญฃ็กฎ่งฃๆžๆ ‡ๅ‡† JSON ๆ•ฐ็ป„', () => {
const result = parseCodeOutput(MOCK_JSON);
expect(result).toHaveLength(2);
expect(result[0].path).toBe('src/main.ts');
expect(result[0].content).toContain('phaser');
expect(result[1].path).toBe('src/scenes/MainScene.ts');
});
it('ๅบ”ๆญฃ็กฎ่งฃๆžๅธฆๆœ‰ Markdown ไปฃ็ ๅ—ๅŒ…่ฃน็š„ JSON', () => {
const wrapped = '```json\n' + MOCK_JSON + '\n```';
const result = parseCodeOutput(wrapped);
expect(result).toHaveLength(2);
expect(result[0].path).toBe('src/main.ts');
});
it('ๅบ”ๆญฃ็กฎ่งฃๆžๅธฆๆœ‰ TypeScript ไปฃ็ ๅ—ๅŒ…่ฃน็š„ JSON', () => {
const wrapped = '```typescript\n' + MOCK_JSON + '\n```';
const result = parseCodeOutput(wrapped);
expect(result).toHaveLength(2);
});
it('ๅบ”ๆญฃ็กฎๅค„็† LLM ่พ“ๅ‡บๅŒ…ๅซๅ‰ๅŽ้ขๅค–ๆ–‡ๆœฌ', () => {
const withExtra = 'Here is the generated code:\n\n' + MOCK_JSON + '\n\nHope this helps!';
const result = parseCodeOutput(withExtra);
expect(result).toHaveLength(2);
expect(result[1].content).toBe('export class MainScene {}');
});
it('ๅบ”ๅค„็† JSON ไธญๅŒ…ๅซ่ฝฌไน‰ๅญ—็ฌฆ็š„ๅ†…ๅฎน', () => {
const files: GeneratedCodeFile[] = [
{ path: 'src/main.ts', content: 'const x = "hello\\nworld";\nconsole.log(x);' },
];
const result = parseCodeOutput(JSON.stringify(files));
expect(result[0].content).toContain('\\n');
});
it('ๅบ”ๅœจ็ฉบ่พ“ๅ‡บๆ—ถๆŠ›ๅ‡บ Error', () => {
expect(() => parseCodeOutput('')).toThrow('empty output');
});
it('ๅบ”ๅœจๅฎŒๅ…จๆ— ๆ•ˆ JSON ๆ—ถๆŠ›ๅ‡บๅธฆๆœ‰้ข„่งˆ็š„ Error', () => {
const garbage = 'this is not json at all, just random text '.repeat(20);
expect(() => parseCodeOutput(garbage)).toThrow('Failed to parse');
try {
parseCodeOutput(garbage);
} catch (err) {
expect((err as Error).message).toContain(garbage.slice(0, 100));
}
});
it('ๅบ”ๆ‹’็ป้žๆ•ฐ็ป„็š„ JSON ่พ“ๅ‡บ', () => {
expect(() => parseCodeOutput('{"path": "a.ts", "content": "b"}')).toThrow('Failed to parse');
});
it('ๅบ”ๆ‹’็ปๆ•ฐ็ป„ๅ…ƒ็ด ็ผบๅฐ‘ path ๆˆ– content ็š„ๆƒ…ๅ†ต', () => {
expect(() => parseCodeOutput('[{"path": "a.ts"}]')).toThrow('Failed to parse');
expect(() => parseCodeOutput('[{"content": "hello"}]')).toThrow('Failed to parse');
});
it('ๅบ”ๅค„็†ๅ•ๆ–‡ไปถๆ•ฐ็ป„', () => {
const single = JSON.stringify([{ path: 'src/game.ts', content: 'export default {};' }]);
const result = parseCodeOutput(single);
expect(result).toHaveLength(1);
expect(result[0].path).toBe('src/game.ts');
});
it('ๅบ”ๅค„็†ๅ†…ๅฎนไธญๅŒ…ๅซๅคงๆ‹ฌๅทๅ’Œๆ–นๆ‹ฌๅท็š„ไปฃ็ ', () => {
const files: GeneratedCodeFile[] = [
{
path: 'src/utils.ts',
content: 'const arr = [1, 2, 3];\nconst obj = { a: { b: [4] } };',
},
];
const result = parseCodeOutput(JSON.stringify(files));
expect(result[0].content).toContain('[1, 2, 3]');
expect(result[0].content).toContain('{ a: { b: [4] } }');
});
});
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
// generateGameCode ้›†ๆˆๆต‹่ฏ•
// โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
describe('generateGameCode', () => {
it('ๅบ”ๆญฃ็กฎ็ป„่ฃ… prompt ๅนถ่ฐƒ็”จ callLLM๏ผŒ่ฟ”ๅ›ž่งฃๆžๅŽ็š„ไปฃ็ ', async () => {
vi.mocked(callLLM).mockResolvedValueOnce(MOCK_JSON);
const result = await generateGameCode({
gdd: VALID_GDD,
assetPack: EMPTY_ASSET_PACK,
templateFiles: TEMPLATE_FILES,
archetype: 'platformer',
});
expect(result.files).toHaveLength(2);
expect(result.entryPoint).toBe('src/main.ts');
expect(callLLM).toHaveBeenCalledTimes(1);
// ้ชŒ่ฏ system prompt ๅŒ…ๅซๆจกๆฟๅ†…ๅฎน
const [messages, model] = vi.mocked(callLLM).mock.calls[0];
expect(messages[0].role).toBe('system');
expect(messages[0].content).toContain('platformer/src/main.ts');
expect(messages[0].content).toContain('Phaser');
expect(typeof model).toBe('string');
});
it('ๅบ”ๅฐ† GDD ๅ’Œ AssetPack ๆณจๅ…ฅ user message', async () => {
vi.mocked(callLLM).mockResolvedValueOnce(MOCK_JSON);
await generateGameCode({
gdd: VALID_GDD,
assetPack: EMPTY_ASSET_PACK,
templateFiles: TEMPLATE_FILES,
archetype: 'platformer',
});
const [messages] = vi.mocked(callLLM).mock.calls[0];
const userMsg = messages[1].content;
expect(userMsg).toContain('Test'); // GDD title
expect(userMsg).toContain('ASSET PACK');
});
it('ๅบ”ๆ นๆฎ tier ๅ‚ๆ•ฐ้€‰ๆ‹ฉไธๅŒ็š„ๆจกๅž‹', async () => {
vi.mocked(callLLM).mockResolvedValueOnce(MOCK_JSON);
await generateGameCode({
gdd: VALID_GDD,
assetPack: EMPTY_ASSET_PACK,
templateFiles: TEMPLATE_FILES,
archetype: 'platformer',
tier: 'enterprise',
});
const [, model] = vi.mocked(callLLM).mock.calls[0];
expect(model).toBe('qwen/qwen3-next-80b-a3b-thinking');
});
it('ๆœชๆŒ‡ๅฎš tier ๆ—ถๅบ”ไฝฟ็”จ้ป˜่ฎคๆจกๅž‹', async () => {
vi.mocked(callLLM).mockResolvedValueOnce(MOCK_JSON);
await generateGameCode({
gdd: VALID_GDD,
assetPack: EMPTY_ASSET_PACK,
templateFiles: TEMPLATE_FILES,
archetype: 'top_down',
});
const [, model] = vi.mocked(callLLM).mock.calls[0];
expect(model).toBe('qwen/qwen3-next-80b-a3b-instruct');
});
it('ๅบ”ๅฐ† system prompt ไธญๆ ‡่ฎฐไธบ "Do not invent new hooks"', async () => {
vi.mocked(callLLM).mockResolvedValueOnce(MOCK_JSON);
await generateGameCode({
gdd: VALID_GDD,
assetPack: EMPTY_ASSET_PACK,
templateFiles: TEMPLATE_FILES,
archetype: 'platformer',
});
const [messages] = vi.mocked(callLLM).mock.calls[0];
expect(messages[0].content).toContain('Do NOT invent new hooks');
});
it('entryPoint ๅบ”ไผ˜ๅ…ˆ้€‰ๆ‹ฉ main.ts', async () => {
const files: GeneratedCodeFile[] = [
{ path: 'src/scenes/Game.ts', content: 'export {}' },
{ path: 'src/main.ts', content: 'import Phaser;' },
];
vi.mocked(callLLM).mockResolvedValueOnce(JSON.stringify(files));
const result = await generateGameCode({
gdd: VALID_GDD,
assetPack: EMPTY_ASSET_PACK,
templateFiles: TEMPLATE_FILES,
archetype: 'platformer',
});
expect(result.entryPoint).toBe('src/main.ts');
});
it('LLM ่ฟ”ๅ›žๆ— ๆ•ˆ่พ“ๅ‡บๆ—ถๅบ”ๆŠ›ๅ‡บ Error', async () => {
vi.mocked(callLLM).mockResolvedValueOnce('not valid json');
await expect(
generateGameCode({
gdd: VALID_GDD,
assetPack: EMPTY_ASSET_PACK,
templateFiles: TEMPLATE_FILES,
archetype: 'platformer',
}),
).rejects.toThrow('Failed to parse');
});
});