Spaces:
Sleeping
Sleeping
| 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'); | |
| }); | |
| }); | |