@playcraft/cli 0.0.49 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,264 +0,0 @@
1
- /**
2
- * fix-ids 命令测试
3
- *
4
- * 覆盖:
5
- * - URL 优先级:CLI 参数 → BACKEND_API_URL → PLAYCRAFT_URL → globalConfig.backendUrl →
6
- * globalConfig.url → DEFAULT_PLAYCRAFT_CLOUD_BASE
7
- * - Token 优先级:CLI 参数 → PLAYCRAFT_TOKEN → globalConfig.token
8
- * - 成功/失败分支
9
- * - 本地开发鉴权绕过(X-Local-User)
10
- */
11
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, } from 'vitest';
12
- import http from 'node:http';
13
- // ─── Mock loadGlobalConfig ────────────────────────────────────────────────────
14
- vi.mock('../config.js', () => ({
15
- loadGlobalConfig: vi.fn(() => ({})),
16
- saveGlobalConfig: vi.fn(),
17
- loadConfig: vi.fn(),
18
- }));
19
- import { loadGlobalConfig } from '../config.js';
20
- import { fixIdsCommand } from './fix-ids.js';
21
- import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from './login.js';
22
- let server;
23
- let baseUrl;
24
- let lastRequest;
25
- let responseStatus = 200;
26
- let responseBody = { success: true };
27
- beforeAll(async () => {
28
- server = http.createServer((req, res) => {
29
- lastRequest = {
30
- url: req.url ?? '',
31
- method: req.method ?? '',
32
- headers: req.headers,
33
- };
34
- res.writeHead(responseStatus, { 'Content-Type': 'application/json' });
35
- res.end(JSON.stringify(responseBody));
36
- });
37
- await new Promise((resolve) => {
38
- server.listen(0, '127.0.0.1', () => resolve());
39
- });
40
- const addr = server.address();
41
- baseUrl = `http://127.0.0.1:${addr.port}`;
42
- });
43
- afterAll(async () => {
44
- await new Promise((resolve) => server.close(() => resolve()));
45
- });
46
- beforeEach(() => {
47
- lastRequest = null;
48
- responseStatus = 200;
49
- responseBody = { success: true };
50
- vi.mocked(loadGlobalConfig).mockReturnValue({});
51
- // 清理鉴权绕过相关环境变量
52
- delete process.env.BACKEND_API_URL;
53
- delete process.env.PLAYCRAFT_URL;
54
- delete process.env.PLAYCRAFT_TOKEN;
55
- delete process.env.NEXT_PUBLIC_LOCAL_AUTH_BYPASS;
56
- delete process.env.NEXT_PUBLIC_LOCAL_USER;
57
- });
58
- afterEach(() => {
59
- delete process.env.BACKEND_API_URL;
60
- delete process.env.PLAYCRAFT_URL;
61
- delete process.env.PLAYCRAFT_TOKEN;
62
- delete process.env.NEXT_PUBLIC_LOCAL_AUTH_BYPASS;
63
- delete process.env.NEXT_PUBLIC_LOCAL_USER;
64
- });
65
- // ─── URL resolution tests ─────────────────────────────────────────────────────
66
- describe('URL 优先级', () => {
67
- it('明确传入 --api-url 时优先使用 CLI 参数', async () => {
68
- vi.mocked(loadGlobalConfig).mockReturnValue({
69
- backendUrl: 'http://should-not-use.example.com',
70
- });
71
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
72
- expect(lastRequest?.url).toBe('/api/projects/123/fix-ids');
73
- // 确保确实打到了我们的测试服务器
74
- expect(lastRequest?.method).toBe('POST');
75
- });
76
- it('无 CLI 参数时从 BACKEND_API_URL 环境变量读取 URL', async () => {
77
- process.env.BACKEND_API_URL = baseUrl;
78
- await fixIdsCommand({ projectId: '123' });
79
- expect(lastRequest?.url).toBe('/api/projects/123/fix-ids');
80
- });
81
- it('无 BACKEND_API_URL 时从 PLAYCRAFT_URL 环境变量读取 URL', async () => {
82
- process.env.PLAYCRAFT_URL = baseUrl;
83
- await fixIdsCommand({ projectId: '123' });
84
- expect(lastRequest?.url).toBe('/api/projects/123/fix-ids');
85
- });
86
- it('环境变量均未设置时,从全局配置 backendUrl 读取', async () => {
87
- vi.mocked(loadGlobalConfig).mockReturnValue({ backendUrl: baseUrl });
88
- await fixIdsCommand({ projectId: '123' });
89
- expect(lastRequest?.url).toBe('/api/projects/123/fix-ids');
90
- });
91
- it('backendUrl 未设置时,从全局配置 url 读取', async () => {
92
- vi.mocked(loadGlobalConfig).mockReturnValue({ url: baseUrl });
93
- await fixIdsCommand({ projectId: '123' });
94
- expect(lastRequest?.url).toBe('/api/projects/123/fix-ids');
95
- });
96
- it('所有来源均未设置时,回退到 DEFAULT_PLAYCRAFT_CLOUD_BASE(不打本地服务器)', async () => {
97
- // 确认默认地址指向公有云,不含 localhost
98
- expect(DEFAULT_PLAYCRAFT_CLOUD_BASE).toMatch(/^https:\/\//);
99
- expect(DEFAULT_PLAYCRAFT_CLOUD_BASE).not.toContain('localhost');
100
- expect(DEFAULT_PLAYCRAFT_CLOUD_BASE).not.toContain('127.0.0.1');
101
- expect(DEFAULT_PLAYCRAFT_CLOUD_BASE).not.toContain(':3001');
102
- });
103
- it('CLI 参数 > 环境变量 > 全局配置(优先级链路)', async () => {
104
- process.env.BACKEND_API_URL = 'http://should-not-use-env.example.com';
105
- vi.mocked(loadGlobalConfig).mockReturnValue({
106
- backendUrl: 'http://should-not-use-config.example.com',
107
- });
108
- // CLI 参数最高优先级:打到我们的测试服务器
109
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
110
- expect(lastRequest?.url).toBe('/api/projects/123/fix-ids');
111
- });
112
- it('URL 尾部 slash 会被正确去除', async () => {
113
- vi.mocked(loadGlobalConfig).mockReturnValue({ url: `${baseUrl}/` });
114
- await fixIdsCommand({ projectId: '123' });
115
- expect(lastRequest?.url).toBe('/api/projects/123/fix-ids');
116
- });
117
- it('projectId 含特殊字符时被正确 encodeURIComponent', async () => {
118
- vi.mocked(loadGlobalConfig).mockReturnValue({ url: baseUrl });
119
- await fixIdsCommand({ projectId: 'abc/def' });
120
- expect(lastRequest?.url).toBe('/api/projects/abc%2Fdef/fix-ids');
121
- });
122
- });
123
- // ─── Token / auth tests ───────────────────────────────────────────────────────
124
- describe('Token 优先级', () => {
125
- it('明确传入 --token 时优先使用 CLI 参数', async () => {
126
- vi.mocked(loadGlobalConfig).mockReturnValue({
127
- url: baseUrl,
128
- token: 'global-token',
129
- });
130
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl, token: 'cli-token' });
131
- expect(lastRequest?.headers['authorization']).toBe('Bearer cli-token');
132
- });
133
- it('CLI 未传 token,从 PLAYCRAFT_TOKEN 环境变量读取', async () => {
134
- process.env.PLAYCRAFT_TOKEN = 'env-token';
135
- vi.mocked(loadGlobalConfig).mockReturnValue({
136
- url: baseUrl,
137
- token: 'global-token-should-not-use',
138
- });
139
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
140
- expect(lastRequest?.headers['authorization']).toBe('Bearer env-token');
141
- });
142
- it('环境变量未设置时,从全局配置 token 读取(login 保存的 PAT)', async () => {
143
- vi.mocked(loadGlobalConfig).mockReturnValue({
144
- url: baseUrl,
145
- token: 'playcraft_pat_from_login',
146
- });
147
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
148
- expect(lastRequest?.headers['authorization']).toBe('Bearer playcraft_pat_from_login');
149
- });
150
- it('三者均未设置时不带 Authorization header', async () => {
151
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
152
- expect(lastRequest?.headers['authorization']).toBeUndefined();
153
- });
154
- it('CLI 参数 > 环境变量 > 全局配置(token 优先级链路)', async () => {
155
- process.env.PLAYCRAFT_TOKEN = 'env-token-should-not-use';
156
- vi.mocked(loadGlobalConfig).mockReturnValue({
157
- url: baseUrl,
158
- token: 'global-token-should-not-use',
159
- });
160
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl, token: 'cli-wins' });
161
- expect(lastRequest?.headers['authorization']).toBe('Bearer cli-wins');
162
- });
163
- });
164
- // ─── 本地开发鉴权绕过 ────────────────────────────────────────────────────────
165
- describe('本地开发鉴权绕过(X-Local-User)', () => {
166
- it('NEXT_PUBLIC_LOCAL_AUTH_BYPASS=true 时附加 X-Local-User header', async () => {
167
- process.env.NEXT_PUBLIC_LOCAL_AUTH_BYPASS = 'true';
168
- process.env.NEXT_PUBLIC_LOCAL_USER = 'admin@example.com';
169
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
170
- expect(lastRequest?.headers['x-local-user']).toBe('admin@example.com');
171
- });
172
- it('NEXT_PUBLIC_LOCAL_AUTH_BYPASS 未设置时不附加 X-Local-User', async () => {
173
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
174
- expect(lastRequest?.headers['x-local-user']).toBeUndefined();
175
- });
176
- it('NEXT_PUBLIC_LOCAL_AUTH_BYPASS=false 时不附加 X-Local-User', async () => {
177
- process.env.NEXT_PUBLIC_LOCAL_AUTH_BYPASS = 'false';
178
- process.env.NEXT_PUBLIC_LOCAL_USER = 'user@example.com';
179
- await fixIdsCommand({ projectId: '123', apiUrl: baseUrl });
180
- expect(lastRequest?.headers['x-local-user']).toBeUndefined();
181
- });
182
- });
183
- // ─── 成功 / 失败分支 ─────────────────────────────────────────────────────────
184
- describe('成功/失败响应处理', () => {
185
- it('成功时不抛出异常', async () => {
186
- responseBody = { success: true };
187
- await expect(fixIdsCommand({ projectId: '123', apiUrl: baseUrl })).resolves.toBeUndefined();
188
- });
189
- it('HTTP 4xx 时调用 process.exit(1)', async () => {
190
- responseStatus = 401;
191
- responseBody = { message: 'Unauthorized' };
192
- const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
193
- throw new Error('process.exit called');
194
- }));
195
- await expect(fixIdsCommand({ projectId: '123', apiUrl: baseUrl })).rejects.toThrow('process.exit called');
196
- expect(mockExit).toHaveBeenCalledWith(1);
197
- mockExit.mockRestore();
198
- });
199
- it('HTTP 500 时调用 process.exit(1)', async () => {
200
- responseStatus = 500;
201
- responseBody = { message: 'Internal Server Error' };
202
- const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
203
- throw new Error('process.exit called');
204
- }));
205
- await expect(fixIdsCommand({ projectId: '123', apiUrl: baseUrl })).rejects.toThrow('process.exit called');
206
- expect(mockExit).toHaveBeenCalledWith(1);
207
- mockExit.mockRestore();
208
- });
209
- it('响应 success 字段为 false 时调用 process.exit(1)', async () => {
210
- responseStatus = 200;
211
- responseBody = { success: false };
212
- const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
213
- throw new Error('process.exit called');
214
- }));
215
- await expect(fixIdsCommand({ projectId: '123', apiUrl: baseUrl })).rejects.toThrow('process.exit called');
216
- expect(mockExit).toHaveBeenCalledWith(1);
217
- mockExit.mockRestore();
218
- });
219
- it('网络错误时调用 process.exit(1)', async () => {
220
- const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
221
- throw new Error('process.exit called');
222
- }));
223
- // 使用一个必定拒绝连接的地址
224
- await expect(fixIdsCommand({ projectId: '123', apiUrl: 'http://127.0.0.1:1' })).rejects.toThrow('process.exit called');
225
- expect(mockExit).toHaveBeenCalledWith(1);
226
- mockExit.mockRestore();
227
- });
228
- });
229
- // ─── AgentApiClient 凭证验证(无 localhost 默认)────────────────────────────
230
- describe('AgentApiClient 凭证行为', () => {
231
- it('PLAYCRAFT_API_URL / PLAYCRAFT_SANDBOX_TOKEN 均未设置时抛出 Missing credentials 错误', async () => {
232
- const origApiUrl = process.env.PLAYCRAFT_API_URL;
233
- const origSandboxToken = process.env.PLAYCRAFT_SANDBOX_TOKEN;
234
- delete process.env.PLAYCRAFT_API_URL;
235
- delete process.env.PLAYCRAFT_SANDBOX_TOKEN;
236
- const { AgentApiClient } = await import('../utils/agent-api-client.js');
237
- expect(() => new AgentApiClient()).toThrow(/Missing PlayCraft API credentials/);
238
- if (origApiUrl !== undefined)
239
- process.env.PLAYCRAFT_API_URL = origApiUrl;
240
- if (origSandboxToken !== undefined)
241
- process.env.PLAYCRAFT_SANDBOX_TOKEN = origSandboxToken;
242
- });
243
- it('AgentApiClient 不使用 localhost:3001 作为默认地址', async () => {
244
- const origApiUrl = process.env.PLAYCRAFT_API_URL;
245
- const origSandboxToken = process.env.PLAYCRAFT_SANDBOX_TOKEN;
246
- delete process.env.PLAYCRAFT_API_URL;
247
- delete process.env.PLAYCRAFT_SANDBOX_TOKEN;
248
- const { AgentApiClient } = await import('../utils/agent-api-client.js');
249
- let errorMessage = '';
250
- try {
251
- new AgentApiClient();
252
- }
253
- catch (e) {
254
- errorMessage = e instanceof Error ? e.message : String(e);
255
- }
256
- // 错误信息不应包含 localhost 地址 —— 不存在隐式降级到本地
257
- expect(errorMessage).not.toContain('localhost');
258
- expect(errorMessage).not.toContain('127.0.0.1');
259
- if (origApiUrl !== undefined)
260
- process.env.PLAYCRAFT_API_URL = origApiUrl;
261
- if (origSandboxToken !== undefined)
262
- process.env.PLAYCRAFT_SANDBOX_TOKEN = origSandboxToken;
263
- });
264
- });
@@ -1,216 +0,0 @@
1
- /**
2
- * playcraft tools research / fetch-url 命令测试
3
- *
4
- * 覆盖:
5
- * - 正确路径:research 返回 answer + citations
6
- * - 正确路径:fetch-url 返回 answer + citations
7
- * - focus 参数透传
8
- * - query 参数透传
9
- * - 后端返回 4xx / 5xx 时抛出错误
10
- * - 鉴权头:sandbox JWT → X-Sandbox-Token;PAT → Authorization: Bearer
11
- * - citation 为空时不崩溃
12
- */
13
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, } from 'vitest';
14
- import http from 'node:http';
15
- import { researchCommand, fetchUrlCommand } from './tools-research.js';
16
- // ─── Mock loadGlobalConfig ─────────────────────────────────────────────────────
17
- vi.mock('../config.js', () => ({
18
- loadGlobalConfig: vi.fn(() => ({})),
19
- saveGlobalConfig: vi.fn(),
20
- loadConfig: vi.fn(),
21
- }));
22
- let server;
23
- let baseUrl;
24
- let lastRequest = null;
25
- let responseStatus = 200;
26
- let responseBody = {
27
- answer: 'Test answer from Gemini',
28
- citations: [],
29
- };
30
- beforeAll(async () => {
31
- server = http.createServer((req, res) => {
32
- let body = '';
33
- req.on('data', (chunk) => { body += chunk.toString(); });
34
- req.on('end', () => {
35
- lastRequest = {
36
- url: req.url ?? '',
37
- method: req.method ?? '',
38
- headers: req.headers,
39
- body,
40
- };
41
- res.writeHead(responseStatus, { 'Content-Type': 'application/json' });
42
- res.end(JSON.stringify(responseBody));
43
- });
44
- });
45
- await new Promise((resolve) => {
46
- server.listen(0, '127.0.0.1', () => resolve());
47
- });
48
- const addr = server.address();
49
- baseUrl = `http://127.0.0.1:${addr.port}`;
50
- });
51
- afterAll(async () => {
52
- await new Promise((resolve) => server.close(() => resolve()));
53
- });
54
- beforeEach(() => {
55
- lastRequest = null;
56
- responseStatus = 200;
57
- responseBody = { answer: 'Test answer', citations: [] };
58
- delete process.env.PLAYCRAFT_API_URL;
59
- delete process.env.PLAYCRAFT_SANDBOX_TOKEN;
60
- });
61
- afterEach(() => {
62
- delete process.env.PLAYCRAFT_API_URL;
63
- delete process.env.PLAYCRAFT_SANDBOX_TOKEN;
64
- });
65
- // ─── Helper ───────────────────────────────────────────────────────────────────
66
- /** 解析测试服务器收到的请求 body */
67
- function parseBody() {
68
- return JSON.parse(lastRequest?.body ?? '{}');
69
- }
70
- // ─── researchCommand ──────────────────────────────────────────────────────────
71
- describe('researchCommand', () => {
72
- it('发送 POST 到 /api/agent/tools/research', async () => {
73
- const result = await researchCommand({
74
- query: 'match-3 level design',
75
- apiUrl: baseUrl,
76
- token: 'test-sandbox-token',
77
- });
78
- expect(lastRequest?.url).toBe('/api/agent/tools/research');
79
- expect(lastRequest?.method).toBe('POST');
80
- expect(result.answer).toBe('Test answer');
81
- });
82
- it('将 query 和 focus 正确写入请求 body', async () => {
83
- await researchCommand({
84
- query: 'pixel art color palette',
85
- focus: 'visual',
86
- apiUrl: baseUrl,
87
- token: 'test-sandbox-token',
88
- });
89
- const body = parseBody();
90
- expect(body.query).toBe('pixel art color palette');
91
- expect(body.focus).toBe('visual');
92
- });
93
- it('不传 focus 时 body 中 focus 为 undefined(不发送该字段)', async () => {
94
- await researchCommand({
95
- query: 'game mechanics',
96
- apiUrl: baseUrl,
97
- token: 'test-sandbox-token',
98
- });
99
- const body = parseBody();
100
- expect(body.query).toBe('game mechanics');
101
- expect(body.focus).toBeUndefined();
102
- });
103
- it('所有 focus 枚举值均可正常透传', async () => {
104
- const focuses = ['gameplay', 'visual', 'audio', 'code', 'compliance'];
105
- for (const focus of focuses) {
106
- await researchCommand({ query: 'test', focus, apiUrl: baseUrl, token: 'tok' });
107
- expect(parseBody().focus).toBe(focus);
108
- }
109
- });
110
- it('返回包含 citations 的完整结果', async () => {
111
- responseBody = {
112
- answer: 'Answer with citations',
113
- citations: [
114
- { url: 'https://example.com/1', title: 'Source 1' },
115
- { url: 'https://example.com/2', title: 'Source 2', excerpt: 'excerpt text' },
116
- ],
117
- };
118
- const result = await researchCommand({
119
- query: 'test query',
120
- apiUrl: baseUrl,
121
- token: 'test-token',
122
- });
123
- expect(result.answer).toBe('Answer with citations');
124
- expect(result.citations).toHaveLength(2);
125
- expect(result.citations[0].url).toBe('https://example.com/1');
126
- expect(result.citations[1].excerpt).toBe('excerpt text');
127
- });
128
- it('citations 为空数组时不崩溃', async () => {
129
- responseBody = { answer: 'No citations answer', citations: [] };
130
- const result = await researchCommand({
131
- query: 'test',
132
- apiUrl: baseUrl,
133
- token: 'test-token',
134
- });
135
- expect(result.citations).toEqual([]);
136
- });
137
- it('使用沙箱 JWT 时发送 X-Sandbox-Token 头', async () => {
138
- await researchCommand({ query: 'test', apiUrl: baseUrl, token: 'sandbox-jwt-eyJ...' });
139
- expect(lastRequest?.headers['x-sandbox-token']).toBe('sandbox-jwt-eyJ...');
140
- expect(lastRequest?.headers['authorization']).toBeUndefined();
141
- });
142
- it('使用 PAT 时发送 Authorization: Bearer 头', async () => {
143
- await researchCommand({ query: 'test', apiUrl: baseUrl, token: 'playcraft_pat_abc123' });
144
- expect(lastRequest?.headers['authorization']).toBe('Bearer playcraft_pat_abc123');
145
- expect(lastRequest?.headers['x-sandbox-token']).toBeUndefined();
146
- });
147
- it('后端返回 400 时抛出错误', async () => {
148
- responseStatus = 400;
149
- responseBody = { error: 'query is required' };
150
- await expect(researchCommand({ query: 'test', apiUrl: baseUrl, token: 'tok' })).rejects.toThrow(/400/);
151
- });
152
- it('后端返回 500 时抛出错误', async () => {
153
- responseStatus = 500;
154
- responseBody = { error: 'Internal server error' };
155
- await expect(researchCommand({ query: 'test', apiUrl: baseUrl, token: 'tok' })).rejects.toThrow(/500/);
156
- });
157
- });
158
- // ─── fetchUrlCommand ──────────────────────────────────────────────────────────
159
- describe('fetchUrlCommand', () => {
160
- it('发送 POST 到 /api/agent/tools/fetch-url', async () => {
161
- const result = await fetchUrlCommand({
162
- url: 'https://phaser.io/docs/3.60.0',
163
- apiUrl: baseUrl,
164
- token: 'test-token',
165
- });
166
- expect(lastRequest?.url).toBe('/api/agent/tools/fetch-url');
167
- expect(lastRequest?.method).toBe('POST');
168
- expect(result.answer).toBe('Test answer');
169
- });
170
- it('将 url 和 query 正确写入请求 body', async () => {
171
- await fetchUrlCommand({
172
- url: 'https://phaser.io/docs/3.60.0',
173
- query: 'how does TileMap collision work',
174
- apiUrl: baseUrl,
175
- token: 'test-token',
176
- });
177
- const body = parseBody();
178
- expect(body.url).toBe('https://phaser.io/docs/3.60.0');
179
- expect(body.query).toBe('how does TileMap collision work');
180
- });
181
- it('不传 query 时请求 body 中 query 为 undefined', async () => {
182
- await fetchUrlCommand({
183
- url: 'https://example.com',
184
- apiUrl: baseUrl,
185
- token: 'tok',
186
- });
187
- const body = parseBody();
188
- expect(body.url).toBe('https://example.com');
189
- expect(body.query).toBeUndefined();
190
- });
191
- it('返回包含 citations 的完整结果', async () => {
192
- responseBody = {
193
- answer: 'Content from URL',
194
- citations: [{ url: 'https://phaser.io/docs', title: 'Phaser Docs' }],
195
- };
196
- const result = await fetchUrlCommand({
197
- url: 'https://phaser.io/docs',
198
- apiUrl: baseUrl,
199
- token: 'tok',
200
- });
201
- expect(result.answer).toBe('Content from URL');
202
- expect(result.citations[0].title).toBe('Phaser Docs');
203
- });
204
- it('使用 PAT 时发送 Authorization: Bearer 头', async () => {
205
- await fetchUrlCommand({
206
- url: 'https://example.com',
207
- apiUrl: baseUrl,
208
- token: 'playcraft_pat_xyz',
209
- });
210
- expect(lastRequest?.headers['authorization']).toBe('Bearer playcraft_pat_xyz');
211
- });
212
- it('后端返回 400 时抛出错误', async () => {
213
- responseStatus = 400;
214
- await expect(fetchUrlCommand({ url: 'https://example.com', apiUrl: baseUrl, token: 'tok' })).rejects.toThrow(/400/);
215
- });
216
- });
@@ -1,49 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { describe, expect, it } from 'vitest';
5
- import { checkPradFile } from './check.js';
6
- import { explainPradPath, formatExplain } from './explain.js';
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const mahjongSample = path.resolve(__dirname, '..', '..', '..', '..', 'docs', 'prad', 'samples', 'mahjong-gate4.atom-tree.json');
9
- describe('prad check', () => {
10
- it('mahjong gate4 v0.2.1 passes with no E026 and draft warnings only', () => {
11
- if (!fs.existsSync(mahjongSample))
12
- return;
13
- const result = checkPradFile(mahjongSample, { gate: 4 });
14
- expect(result.errors).toEqual([]);
15
- expect(result.warnings.some((w) => w.code === 'PRAD-W020')).toBe(true);
16
- expect(result.warnings.some((w) => w.code === 'PRAD-E026')).toBe(false);
17
- expect(result.errors.some((e) => e.code === 'PRAD-E026')).toBe(false);
18
- expect(result.ok).toBe(true);
19
- });
20
- it('explain play_btn path', () => {
21
- if (!fs.existsSync(mahjongSample))
22
- return;
23
- const doc = JSON.parse(fs.readFileSync(mahjongSample, 'utf-8'));
24
- const result = explainPradPath(doc, 'scenes.entry.ui_layer.play_btn');
25
- expect(result.found).toBe(true);
26
- expect(result.slot?.spine).toBe('medium.hook.play');
27
- expect(formatExplain(result)).toContain('medium.hook.play');
28
- });
29
- it('explain rule_engine itemSprites uses pick_and_match manifest', () => {
30
- if (!fs.existsSync(mahjongSample))
31
- return;
32
- const doc = JSON.parse(fs.readFileSync(mahjongSample, 'utf-8'));
33
- const result = explainPradPath(doc, 'scenes.gameplay.board_layer.rule_engine.props.itemSprites');
34
- expect(result.found).toBe(true);
35
- expect(result.bindAs).toBe('itemSprites');
36
- expect(result.parentEffectiveAtom).toBe('pick_and_match_rules.aigameplay');
37
- expect(result.manifestImportSlots).toContain('itemSprites');
38
- });
39
- it('explain hint_btn icon bindAs fingerTexture on tutorial_overlay', () => {
40
- if (!fs.existsSync(mahjongSample))
41
- return;
42
- const doc = JSON.parse(fs.readFileSync(mahjongSample, 'utf-8'));
43
- const result = explainPradPath(doc, 'scenes.gameplay.hud_layer.hint_btn.props.icon');
44
- expect(result.found).toBe(true);
45
- expect(result.bindAs).toBe('fingerTexture');
46
- expect(result.parentEffectiveAtom).toBe('tutorial_overlay.aicomponent');
47
- expect(result.manifestImportSlots).toContain('fingerTexture');
48
- });
49
- });
@@ -1,80 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import path from 'node:path';
3
- import fs from 'node:fs/promises';
4
- import os from 'node:os';
5
- import { SyncManager } from './sync-manager.js';
6
- describe('SyncManager', () => {
7
- let tmpDir;
8
- let mockConnection;
9
- beforeEach(async () => {
10
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sync-manager-test-'));
11
- mockConnection = {
12
- request: vi.fn(),
13
- isConnected: true,
14
- connectionState: 'connected',
15
- };
16
- });
17
- afterEach(async () => {
18
- if (tmpDir) {
19
- await fs.rm(tmpDir, { recursive: true, force: true });
20
- }
21
- });
22
- it('upload() reads file and calls cloud request', async () => {
23
- const filePath = 'test.txt';
24
- const fullPath = path.join(tmpDir, filePath);
25
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
26
- await fs.writeFile(fullPath, 'hello', 'utf-8');
27
- vi.mocked(mockConnection.request).mockResolvedValue({ success: true });
28
- const manager = new SyncManager({
29
- cloudConnection: mockConnection,
30
- projectDir: tmpDir,
31
- });
32
- const result = await manager.upload(filePath);
33
- expect(result.success).toBe(true);
34
- expect(result.direction).toBe('up');
35
- expect(mockConnection.request).toHaveBeenCalledWith('POST', '/api/sync/upload', expect.objectContaining({
36
- path: filePath,
37
- data: expect.any(String),
38
- compressed: false,
39
- }));
40
- });
41
- it('upload() returns error result when file missing', async () => {
42
- const manager = new SyncManager({
43
- cloudConnection: mockConnection,
44
- projectDir: tmpDir,
45
- });
46
- const result = await manager.upload('nonexistent.txt');
47
- expect(result.success).toBe(false);
48
- expect(result.error).toBeDefined();
49
- expect(mockConnection.request).not.toHaveBeenCalled();
50
- });
51
- it('download() calls cloud request and writes file', async () => {
52
- const filePath = 'subdir/file.txt';
53
- const content = Buffer.from('world').toString('base64');
54
- vi.mocked(mockConnection.request).mockResolvedValue({ data: content });
55
- const manager = new SyncManager({
56
- cloudConnection: mockConnection,
57
- projectDir: tmpDir,
58
- });
59
- const result = await manager.download(filePath);
60
- expect(result.success).toBe(true);
61
- expect(result.direction).toBe('down');
62
- const fullPath = path.join(tmpDir, filePath);
63
- const written = await fs.readFile(fullPath, 'utf-8');
64
- expect(written).toBe('world');
65
- });
66
- it('getPendingUploads returns empty array', () => {
67
- const manager = new SyncManager({
68
- cloudConnection: mockConnection,
69
- projectDir: tmpDir,
70
- });
71
- expect(manager.getPendingUploads()).toEqual([]);
72
- });
73
- it('getPendingDownloads returns empty array', () => {
74
- const manager = new SyncManager({
75
- cloudConnection: mockConnection,
76
- projectDir: tmpDir,
77
- });
78
- expect(manager.getPendingDownloads()).toEqual([]);
79
- });
80
- });