@ttmg/cli 0.4.5 → 0.4.6-agent-beta.1

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.
Files changed (82) hide show
  1. package/README.md +644 -2
  2. package/dist/index.js +22502 -6070
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +36 -4
  5. package/dist/public/assets/{Card-BAsimNoh.js → Card-CN2lxY0Y.js} +1 -1
  6. package/dist/public/assets/{Detail-rVxbEDgh.js → Detail-CELZZ-4h.js} +1 -1
  7. package/dist/public/assets/Detail-CELZZ-4h.js.br +0 -0
  8. package/dist/public/assets/{MonetizationMode-B20TAE9o.js → MonetizationMode-C7DNqium.js} +1 -1
  9. package/dist/public/assets/{MonetizationModeSummary-CnHqXyco.js → MonetizationModeSummary-DYE7XUq1.js} +1 -1
  10. package/dist/public/assets/{SectionHeader-Cn3FnOqh.js → SectionHeader-BKc3BWXP.js} +1 -1
  11. package/dist/public/assets/{baseForm-kBTxn8h8.js → baseForm-I9suIiQn.js} +1 -1
  12. package/dist/public/assets/baseForm-I9suIiQn.js.br +0 -0
  13. package/dist/public/assets/{col-VN3Ivd-x.js → col-w9Q-2mgM.js} +1 -1
  14. package/dist/public/assets/{compass-BabZVjAl.js → compass-D6waJ52O.js} +1 -1
  15. package/dist/public/assets/{index-DHdJsD10.js → index-8sDAOAcS.js} +1 -1
  16. package/dist/public/assets/index-8sDAOAcS.js.br +0 -0
  17. package/dist/public/assets/{index-DK4IvdRl.js → index-B6hY5GdS.js} +1 -1
  18. package/dist/public/assets/{index-Cl0_vMWE.js → index-BIrFkB6r.js} +1 -1
  19. package/dist/public/assets/index-BIrFkB6r.js.br +0 -0
  20. package/dist/public/assets/{index-BjQlJrX7.js → index-BLWgpW64.js} +1 -1
  21. package/dist/public/assets/{index-odEI7YQF.js → index-BY3eQmfP.js} +1 -1
  22. package/dist/public/assets/index-BY3eQmfP.js.br +0 -0
  23. package/dist/public/assets/{index-Bjn0ctEd.js → index-BegU9LBT.js} +2 -2
  24. package/dist/public/assets/index-BegU9LBT.js.br +0 -0
  25. package/dist/public/assets/{index-1WjGwqIV.js → index-BqwflQml.js} +1 -1
  26. package/dist/public/assets/{index-DXDZCuG4.js → index-BrOFdVbv.js} +1 -1
  27. package/dist/public/assets/{index-BxsB5E3u.js → index-BvPolBSn.js} +1 -1
  28. package/dist/public/assets/{index-BnOeYQAd.js → index-BwrZjkCc.js} +1 -1
  29. package/dist/public/assets/{index-CUuX90R9.js → index-CLKKui8G.js} +1 -1
  30. package/dist/public/assets/{index-Dohibgdp.js → index-Cb4Ja0tZ.js} +1 -1
  31. package/dist/public/assets/{index-C83QHLmD.js → index-CcUgN29Y.js} +1 -1
  32. package/dist/public/assets/{index-aSl6Wdrp.js → index-CgXOkYrX.js} +1 -1
  33. package/dist/public/assets/index-CgXOkYrX.js.br +0 -0
  34. package/dist/public/assets/{index-DulANwbq.js → index-CsiIrLKY.js} +1 -1
  35. package/dist/public/assets/{index-Ba-JWe4X.js → index-Csmg5oyu.js} +1 -1
  36. package/dist/public/assets/{index-DnnfZ7H_.js → index-DDecPuod.js} +1 -1
  37. package/dist/public/assets/{index-Clq0hTDI.js → index-DDy4PkWT.js} +1 -1
  38. package/dist/public/assets/{index-DIcz5ciL.js → index-DEQ2hVPA.js} +1 -1
  39. package/dist/public/assets/index-DEQ2hVPA.js.br +0 -0
  40. package/dist/public/assets/{index-CpM8t743.js → index-DGkgqtgJ.js} +1 -1
  41. package/dist/public/assets/{index-QNLvejKK.js → index-FVHabbDB.js} +1 -1
  42. package/dist/public/assets/{index-CzXMNZL3.js → index-M8jf-YAZ.js} +1 -1
  43. package/dist/public/assets/{index-BxokHznc.js → index-Q0GC8Irj.js} +1 -1
  44. package/dist/public/assets/index-Q0GC8Irj.js.br +0 -0
  45. package/dist/public/assets/{index--6iHQzho.js → index-YVB9ZW00.js} +1 -1
  46. package/dist/public/assets/{index-O_L1tU2b.js → index-mjEeEQcC.js} +1 -1
  47. package/dist/public/assets/index-mjEeEQcC.js.br +0 -0
  48. package/dist/public/assets/{index-BFFsJ47B.js → index-rLkONFpx.js} +1 -1
  49. package/dist/public/assets/{layers-B4zhx14P.js → layers-Dbwq9AyL.js} +1 -1
  50. package/dist/public/assets/{list-checks-Br6imjXC.js → list-checks-og1usOZf.js} +1 -1
  51. package/dist/public/assets/{sparkles-BjIkcbAi.js → sparkles-p-dZbjGb.js} +1 -1
  52. package/dist/public/assets/{triangle-alert-CgKAc3w5.js → triangle-alert-BZVAEUiL.js} +1 -1
  53. package/dist/public/assets/{zap-DFHw5liD.js → zap-BAhZUPPa.js} +1 -1
  54. package/dist/public/index.html +1 -1
  55. package/dist/scripts/acceptance-fixture.test.js +33 -0
  56. package/dist/scripts/dev-capture-smoke.js +76 -0
  57. package/dist/scripts/dev-client-preview-control-smoke.js +606 -0
  58. package/dist/scripts/dev-default-smoke.js +67 -0
  59. package/dist/scripts/dev-game-ready-smoke.js +155 -0
  60. package/dist/scripts/dev-workflow-smoke.js +260 -0
  61. package/dist/scripts/prepare-client-preview-acceptance.js +65 -0
  62. package/dist/scripts/preview-page-server-smoke.js +63 -0
  63. package/dist/scripts/run-ts-test.js +41 -0
  64. package/dist/scripts/verify-agent-contract.js +410 -0
  65. package/dist/scripts/vibe-ppe-test-preload.cjs +23 -0
  66. package/dist/scripts/vibe-upload-demo.js +72 -0
  67. package/dist/scripts/vibe-upload-smoke.js +412 -0
  68. package/dist/scripts/wasm-smoke.js +53 -0
  69. package/dist/scripts/wasm-worker.js +63 -0
  70. package/dist/scripts/worker.js +28 -22
  71. package/package.json +36 -4
  72. package/CHANGELOG.md +0 -277
  73. package/dist/public/assets/Detail-rVxbEDgh.js.br +0 -0
  74. package/dist/public/assets/baseForm-kBTxn8h8.js.br +0 -0
  75. package/dist/public/assets/index-Bjn0ctEd.js.br +0 -0
  76. package/dist/public/assets/index-BxokHznc.js.br +0 -0
  77. package/dist/public/assets/index-Cl0_vMWE.js.br +0 -0
  78. package/dist/public/assets/index-DHdJsD10.js.br +0 -0
  79. package/dist/public/assets/index-DIcz5ciL.js.br +0 -0
  80. package/dist/public/assets/index-O_L1tU2b.js.br +0 -0
  81. package/dist/public/assets/index-aSl6Wdrp.js.br +0 -0
  82. package/dist/public/assets/index-odEI7YQF.js.br +0 -0
@@ -0,0 +1,606 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const net = require('node:net');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawn } = require('node:child_process');
7
+
8
+ function runCli(args, cwd, inspectScanPage) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(
11
+ process.execPath,
12
+ [path.resolve(__dirname, '../dist/index.js'), ...args],
13
+ {
14
+ cwd,
15
+ env: {
16
+ ...process.env,
17
+ NO_COLOR: '1',
18
+ FORCE_COLOR: '0',
19
+ TTMG_DEV_NO_OPEN: '1',
20
+ },
21
+ stdio: ['ignore', 'pipe', 'pipe'],
22
+ },
23
+ );
24
+ let stdout = '';
25
+ let stderr = '';
26
+ let inspection = Promise.resolve();
27
+ let inspectionStarted = false;
28
+ let inspectionError;
29
+ const timer = setTimeout(() => child.kill('SIGKILL'), 20000);
30
+ child.stdout.on('data', chunk => {
31
+ stdout += chunk;
32
+ if (inspectScanPage && !inspectionStarted) {
33
+ for (const line of stdout.split(/\r?\n/)) {
34
+ let event;
35
+ try {
36
+ event = JSON.parse(line);
37
+ } catch {
38
+ continue;
39
+ }
40
+ if (event.payload?.state !== 'READY_FOR_SCAN') continue;
41
+ inspectionStarted = true;
42
+ inspection = inspectScanPage(event.payload).catch(error => {
43
+ inspectionError = error;
44
+ });
45
+ break;
46
+ }
47
+ }
48
+ });
49
+ child.stderr.on('data', chunk => {
50
+ stderr += chunk;
51
+ });
52
+ child.once('error', error => {
53
+ clearTimeout(timer);
54
+ reject(error);
55
+ });
56
+ child.once('close', async status => {
57
+ clearTimeout(timer);
58
+ await inspection;
59
+ if (inspectionError) {
60
+ reject(inspectionError);
61
+ return;
62
+ }
63
+ if (inspectScanPage && !inspectionStarted) {
64
+ reject(new Error(`No live scan page: ${stderr || stdout}`));
65
+ return;
66
+ }
67
+ resolve({ status, stdout, stderr });
68
+ });
69
+ });
70
+ }
71
+
72
+ function write(socket, value) {
73
+ socket.write(`${JSON.stringify(value)}\n`);
74
+ }
75
+
76
+ async function run() {
77
+ const shortTmp = process.platform === 'win32' ? os.tmpdir() : '/tmp';
78
+ const root = fs.mkdtempSync(path.join(shortTmp, 'ttmg-preview-control-'));
79
+ const gameDirectory = path.join(root, 'game');
80
+ const previewGameDirectory = path.join(
81
+ root,
82
+ `client-preview-project-${'x'.repeat(80)}`,
83
+ );
84
+ const sessionId = 'dev_control_smoke';
85
+ const sessionDirectory = path.join(
86
+ gameDirectory,
87
+ '.ttmg',
88
+ 'sessions',
89
+ sessionId,
90
+ );
91
+ const screenshotsDirectory = path.join(sessionDirectory, 'screenshots');
92
+ fs.mkdirSync(screenshotsDirectory, { recursive: true });
93
+ fs.mkdirSync(previewGameDirectory, { recursive: true });
94
+ fs.writeFileSync(
95
+ path.join(previewGameDirectory, 'game.js'),
96
+ 'console.log("game");\n',
97
+ );
98
+ fs.writeFileSync(path.join(previewGameDirectory, 'game.json'), '{}\n');
99
+ const control =
100
+ process.platform === 'win32'
101
+ ? {
102
+ transport: 'named-pipe',
103
+ path: `\\\\.\\pipe\\ttmg-${sessionId}`,
104
+ }
105
+ : {
106
+ transport: 'unix',
107
+ path: path.join(sessionDirectory, 'ipc'),
108
+ };
109
+ fs.writeFileSync(
110
+ path.join(sessionDirectory, 'manifest.json'),
111
+ JSON.stringify({
112
+ schemaVersion: '1',
113
+ command: 'dev',
114
+ platformWrite: false,
115
+ sessionId,
116
+ status: 'ready',
117
+ mode: 'client-preview',
118
+ project: { clientKey: 'smoke-client', directory: gameDirectory },
119
+ process: { pid: process.pid, startedAt: Date.now(), command: 'ttmg dev' },
120
+ runtime: {
121
+ status: 'ready',
122
+ runtimeSessionId: 'runtime-control-smoke',
123
+ protocolVersion: '1.0',
124
+ capabilities: { logs: true, screenshot: true },
125
+ },
126
+ control,
127
+ }),
128
+ );
129
+ const server = net.createServer(socket => {
130
+ let buffer = '';
131
+ socket.on('data', chunk => {
132
+ buffer += chunk.toString();
133
+ if (!buffer.includes('\n')) return;
134
+ const request = JSON.parse(buffer.slice(0, buffer.indexOf('\n')));
135
+ if (request.operation === 'session.status') {
136
+ write(socket, {
137
+ requestId: request.requestId,
138
+ type: 'result',
139
+ result: { status: 'ready' },
140
+ });
141
+ socket.end();
142
+ return;
143
+ }
144
+ if (request.operation === 'logs.snapshot') {
145
+ write(socket, {
146
+ requestId: request.requestId,
147
+ type: 'result',
148
+ result: {
149
+ records: [
150
+ {
151
+ sequence: '90071992547409930002',
152
+ timestamp_ms: 2,
153
+ severity: 'info',
154
+ kind: 'text',
155
+ source: 'native_runtime',
156
+ message: 'snapshot',
157
+ formatted_line: '[I][native_runtime] snapshot',
158
+ },
159
+ ],
160
+ nextCursor: '90071992547409930002',
161
+ oldestCursor: '90071992547409930001',
162
+ snapshotCursor: '90071992547409930002',
163
+ hasMore: false,
164
+ cursorExpired: false,
165
+ },
166
+ });
167
+ socket.end();
168
+ return;
169
+ }
170
+ if (request.operation === 'logs.follow') {
171
+ write(socket, {
172
+ requestId: request.requestId,
173
+ type: 'result',
174
+ result: {
175
+ records: [],
176
+ nextCursor: request.params.after,
177
+ oldestCursor: '1',
178
+ snapshotCursor: request.params.after,
179
+ hasMore: false,
180
+ cursorExpired: false,
181
+ },
182
+ });
183
+ write(socket, {
184
+ requestId: request.requestId,
185
+ type: 'event',
186
+ records: [
187
+ {
188
+ sequence: '90071992547409930003',
189
+ timestamp_ms: 3,
190
+ severity: 'error',
191
+ kind: 'text',
192
+ source: 'structured_trace',
193
+ message: 'live',
194
+ formatted_line: '[E][structured_trace] live',
195
+ },
196
+ ],
197
+ cursor: '90071992547409930003',
198
+ });
199
+ write(socket, {
200
+ requestId: request.requestId,
201
+ type: 'end',
202
+ cursor: '90071992547409930003',
203
+ reason: 'timeout',
204
+ });
205
+ socket.end();
206
+ return;
207
+ }
208
+ if (request.operation === 'screenshot.capture') {
209
+ if (
210
+ request.params.runtimeSessionId &&
211
+ request.params.runtimeSessionId !== 'runtime-control-smoke'
212
+ ) {
213
+ write(socket, {
214
+ requestId: request.requestId,
215
+ type: 'error',
216
+ errorCode: 'RUNTIME_SESSION_MISMATCH',
217
+ error: 'Runtime session changed.',
218
+ });
219
+ socket.end();
220
+ return;
221
+ }
222
+ assert.equal(request.params.format, 'jpeg');
223
+ assert.equal(request.params.quality, 0.8);
224
+ const output =
225
+ request.params.output ||
226
+ path.join(screenshotsDirectory, 'default.jpeg');
227
+ fs.writeFileSync(output, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
228
+ write(socket, {
229
+ requestId: request.requestId,
230
+ type: 'result',
231
+ result: {
232
+ path: output,
233
+ runtimeSessionId: 'runtime-control-smoke',
234
+ mime: 'image/jpeg',
235
+ size: 4,
236
+ width: 1,
237
+ height: 1,
238
+ captureTime: '1787700000789',
239
+ sha256: 'smoke-hash',
240
+ },
241
+ });
242
+ socket.end();
243
+ }
244
+ });
245
+ });
246
+
247
+ try {
248
+ const previewRun = await runCli(
249
+ [
250
+ 'dev',
251
+ '--mode',
252
+ 'client-preview',
253
+ '--client-key',
254
+ 'smoke-client',
255
+ '--events',
256
+ 'ndjson',
257
+ '--no-open',
258
+ '--record-logs',
259
+ '--timeout',
260
+ '3',
261
+ ],
262
+ previewGameDirectory,
263
+ async ready => {
264
+ const pageResponse = await fetch(ready.scanPageUrl, {
265
+ signal: AbortSignal.timeout(1500),
266
+ });
267
+ assert.equal(pageResponse.status, 200);
268
+ assert.match(
269
+ pageResponse.headers.get('content-security-policy'),
270
+ /frame-ancestors 'none'/,
271
+ );
272
+ assert.equal(
273
+ pageResponse.headers.get('access-control-allow-origin'),
274
+ null,
275
+ 'scan page must bypass game CORS middleware',
276
+ );
277
+ const html = await pageResponse.text();
278
+ assert.match(html, /id="status-title"/);
279
+ const status = await (
280
+ await fetch(`${ready.scanPageUrl}/status`)
281
+ ).json();
282
+ assert.equal(status.state, 'waiting');
283
+ assert.equal(status.compile, 'succeeded');
284
+ assert.equal(status.developerOptions.values.enableDomainCheck, true);
285
+ const editToken = html.match(/editToken="([a-f0-9]+)"/)[1];
286
+ const saved = await fetch(`${ready.scanPageUrl}/settings`, {
287
+ method: 'POST', headers: { 'Content-Type': 'application/json',
288
+ Origin: new URL(ready.scanPageUrl).origin, 'X-TTMG-Scan-Token': editToken },
289
+ body: JSON.stringify({ ...status.developerOptions.values, enableAdMock: true }),
290
+ });
291
+ assert.equal(saved.status, 200);
292
+ const savedOptions = (await saved.json()).developerOptions;
293
+ assert.equal(savedOptions.values.enableAdMock, true);
294
+ assert.equal(savedOptions.delivery, 'pending');
295
+ assert.deepEqual((await (await fetch(`${ready.scanPageUrl}/status`)).json()).developerOptions, savedOptions);
296
+ const png = Buffer.from(
297
+ await (await fetch(`${ready.scanPageUrl}/qr.png`)).arrayBuffer(),
298
+ );
299
+ assert.deepEqual(
300
+ png,
301
+ fs.readFileSync(ready.qrPngPath),
302
+ 'page and terminal PNG must use the same launch payload',
303
+ );
304
+ },
305
+ );
306
+ assert.equal(previewRun.status, 2, previewRun.stderr || previewRun.stdout);
307
+ const previewEvents = previewRun.stdout
308
+ .trim()
309
+ .split(/\r?\n/)
310
+ .filter(Boolean)
311
+ .map(JSON.parse);
312
+ const readyForScan = previewEvents.find(
313
+ event =>
314
+ event.type === 'lifecycle' && event.payload?.state === 'READY_FOR_SCAN',
315
+ );
316
+ assert.ok(readyForScan, previewRun.stderr || previewRun.stdout);
317
+ assert.equal(fs.existsSync(readyForScan.payload.qrPngPath), true);
318
+ await assert.rejects(
319
+ fetch(readyForScan.payload.scanPageUrl, {
320
+ signal: AbortSignal.timeout(1000),
321
+ }),
322
+ 'scan page stops with the CLI',
323
+ );
324
+ assert.match(
325
+ readyForScan.payload.scanPageUrl,
326
+ /^http:\/\/127\.0\.0\.1:\d+\/__ttmg\/client-preview\/[a-f0-9]+$/,
327
+ );
328
+ const previewManifest = JSON.parse(
329
+ fs.readFileSync(readyForScan.payload.manifest, 'utf8'),
330
+ );
331
+ assert.ok(previewManifest.evidence.runtimeLogs);
332
+ const savedRead = await runCli(
333
+ [
334
+ 'dev',
335
+ 'logs',
336
+ '--source',
337
+ 'archive',
338
+ '--session',
339
+ previewManifest.sessionId,
340
+ '--format',
341
+ 'json',
342
+ ],
343
+ previewGameDirectory,
344
+ );
345
+ assert.equal(savedRead.status, 0, savedRead.stderr || savedRead.stdout);
346
+ const saved = JSON.parse(savedRead.stdout);
347
+ assert.equal(saved.source, 'archive');
348
+ assert.equal(saved.archive.state, 'closed');
349
+ assert.deepEqual(saved.records, []);
350
+ assert.equal(saved.runtimeSessionId, null);
351
+ assert.equal(saved.platformWrite, false);
352
+ if (previewManifest.control?.transport === 'unix') {
353
+ assert.equal(
354
+ Buffer.byteLength(previewManifest.control.path) <= 100,
355
+ true,
356
+ );
357
+ }
358
+ assert.match(previewRun.stderr, /QR image:/);
359
+ assert.equal(
360
+ previewEvents.filter(
361
+ event =>
362
+ event.type === 'compile' && event.payload?.state === 'succeeded',
363
+ ).length,
364
+ 1,
365
+ 'session evidence must not trigger an endless recompilation loop',
366
+ );
367
+
368
+ const humanPreview = await runCli(
369
+ [
370
+ 'dev',
371
+ '--mode',
372
+ 'client-preview',
373
+ '--client-key',
374
+ 'smoke-client',
375
+ '--timeout',
376
+ '1',
377
+ ],
378
+ previewGameDirectory,
379
+ );
380
+ assert.equal(
381
+ humanPreview.status,
382
+ 2,
383
+ humanPreview.stderr || humanPreview.stdout,
384
+ );
385
+ assert.match(humanPreview.stdout, /Scan with TikTok|使用 TikTok 扫码/);
386
+ assert.match(humanPreview.stdout, /QR image:|二维码图片:/);
387
+ assert.match(humanPreview.stdout, /ttmg dev screenshot/);
388
+ assert.ok(!humanPreview.stdout.includes('"schemaVersion"'));
389
+ for (const id of fs.readdirSync(
390
+ path.join(previewGameDirectory, '.ttmg', 'sessions'),
391
+ )) {
392
+ if (id === previewManifest.sessionId) continue;
393
+ const base = path.join(previewGameDirectory, '.ttmg', 'sessions', id);
394
+ const unrecorded = JSON.parse(
395
+ fs.readFileSync(path.join(base, 'manifest.json'), 'utf8'),
396
+ );
397
+ assert.equal(unrecorded.evidence.runtimeLogs, undefined);
398
+ assert.equal(fs.existsSync(path.join(base, 'runtime-logs')), false);
399
+ }
400
+
401
+ await new Promise((resolve, reject) => {
402
+ server.once('error', reject);
403
+ server.listen(control.path, resolve);
404
+ });
405
+ if (control.transport === 'unix') fs.chmodSync(control.path, 0o600);
406
+
407
+ const snapshotRun = await runCli(
408
+ [
409
+ 'dev',
410
+ 'session',
411
+ 'logs',
412
+ sessionId,
413
+ '--after',
414
+ '90071992547409930001',
415
+ '--format',
416
+ 'json',
417
+ ],
418
+ gameDirectory,
419
+ );
420
+ assert.equal(
421
+ snapshotRun.status,
422
+ 0,
423
+ snapshotRun.stderr || snapshotRun.stdout,
424
+ );
425
+ const snapshotLines = snapshotRun.stdout.trim().split(/\r?\n/);
426
+ assert.equal(snapshotLines.length, 1);
427
+ const snapshot = JSON.parse(snapshotLines[0]);
428
+ assert.equal(snapshot.command, 'dev.session.logs');
429
+ assert.equal(snapshot.records[0].sequence, '90071992547409930002');
430
+ assert.equal(typeof snapshot.records[0].sequence, 'string');
431
+
432
+ const followRun = await runCli(
433
+ [
434
+ 'dev',
435
+ 'session',
436
+ 'logs',
437
+ sessionId,
438
+ '--after',
439
+ '90071992547409930002',
440
+ '--follow',
441
+ '--timeout',
442
+ '1',
443
+ '--format',
444
+ 'ndjson',
445
+ ],
446
+ gameDirectory,
447
+ );
448
+ assert.equal(followRun.status, 0, followRun.stderr || followRun.stdout);
449
+ const frames = followRun.stdout.trim().split(/\r?\n/).map(JSON.parse);
450
+ assert.deepEqual(
451
+ frames.map(frame => frame.type),
452
+ ['log_snapshot', 'log_batch', 'follow_end'],
453
+ );
454
+ assert.equal(
455
+ frames.every(frame => frame.platformWrite === false),
456
+ true,
457
+ );
458
+
459
+ const screenshotOutput = path.join(root, 'screen.jpeg');
460
+ const screenshotRun = await runCli(
461
+ [
462
+ 'dev',
463
+ 'session',
464
+ 'screenshot',
465
+ sessionId,
466
+ '--output',
467
+ screenshotOutput,
468
+ '--format',
469
+ 'json',
470
+ ],
471
+ gameDirectory,
472
+ );
473
+ assert.equal(
474
+ screenshotRun.status,
475
+ 0,
476
+ screenshotRun.stderr || screenshotRun.stdout,
477
+ );
478
+ const screenshotLines = screenshotRun.stdout.trim().split(/\r?\n/);
479
+ assert.equal(screenshotLines.length, 1);
480
+ const screenshot = JSON.parse(screenshotLines[0]);
481
+ assert.equal(screenshot.path, screenshotOutput);
482
+ assert.equal(screenshot.runtimeSessionId, 'runtime-control-smoke');
483
+ assert.equal(screenshot.mime, 'image/jpeg');
484
+ assert.equal('data' in screenshot, false);
485
+ assert.equal(fs.readFileSync(screenshotOutput).toString('hex'), 'ffd8ffd9');
486
+ for (const command of [
487
+ ['dev', 'screenshot', '--session', sessionId],
488
+ ['dev', 'session', 'screenshot', sessionId],
489
+ ]) {
490
+ const pinnedRun = await runCli(
491
+ [
492
+ ...command,
493
+ '--runtime-session',
494
+ 'runtime-control-smoke',
495
+ '--output',
496
+ path.join(root, `pinned-${command[1]}.jpeg`),
497
+ '--format',
498
+ 'json',
499
+ ],
500
+ gameDirectory,
501
+ );
502
+ assert.equal(pinnedRun.status, 0, pinnedRun.stderr || pinnedRun.stdout);
503
+ const lines = pinnedRun.stdout.trim().split(/\r?\n/);
504
+ assert.equal(lines.length, 1);
505
+ assert.equal(
506
+ JSON.parse(lines[0]).runtimeSessionId,
507
+ 'runtime-control-smoke',
508
+ );
509
+ const invalidOutput = path.join(root, `stale-${command[1]}.jpeg`);
510
+ const staleRun = await runCli(
511
+ [
512
+ ...command,
513
+ '--runtime-session',
514
+ 'old-runtime',
515
+ '--output',
516
+ invalidOutput,
517
+ '--format',
518
+ 'json',
519
+ ],
520
+ gameDirectory,
521
+ );
522
+ assert.equal(staleRun.status, 1);
523
+ assert.equal(
524
+ JSON.parse(staleRun.stdout).errorCode,
525
+ 'RUNTIME_SESSION_MISMATCH',
526
+ );
527
+ assert.equal(fs.existsSync(invalidOutput), false);
528
+ }
529
+
530
+ // Professional-developer shortcuts use the same IPC, without a session ID.
531
+ const subdirectory = path.join(gameDirectory, 'src');
532
+ fs.mkdirSync(subdirectory);
533
+ const humanLogs = await runCli(['dev', 'logs'], subdirectory);
534
+ assert.equal(humanLogs.status, 0, humanLogs.stderr || humanLogs.stdout);
535
+ assert.match(humanLogs.stdout, /\[INFO\] \[native_runtime\] snapshot/);
536
+ const jsonLogs = await runCli(
537
+ ['dev', 'logs', '--format', 'json'],
538
+ gameDirectory,
539
+ );
540
+ assert.equal(jsonLogs.status, 0, jsonLogs.stderr || jsonLogs.stdout);
541
+ assert.equal(
542
+ JSON.parse(jsonLogs.stdout).records[0].sequence,
543
+ '90071992547409930002',
544
+ );
545
+ const humanFollow = await runCli(
546
+ ['dev', 'logs', '--follow', '--timeout', '1'],
547
+ gameDirectory,
548
+ );
549
+ assert.equal(
550
+ humanFollow.status,
551
+ 0,
552
+ humanFollow.stderr || humanFollow.stdout,
553
+ );
554
+ assert.match(humanFollow.stdout, /\[ERROR\] \[structured_trace\] live/);
555
+ const aliasFollow = await runCli(
556
+ ['dev', 'logs', '--follow', '--timeout', '1', '--format', 'ndjson'],
557
+ gameDirectory,
558
+ );
559
+ assert.equal(
560
+ aliasFollow.status,
561
+ 0,
562
+ aliasFollow.stderr || aliasFollow.stdout,
563
+ );
564
+ assert.deepEqual(
565
+ aliasFollow.stdout
566
+ .trim()
567
+ .split(/\r?\n/)
568
+ .map(JSON.parse)
569
+ .map(frame => frame.type),
570
+ ['log_snapshot', 'log_batch', 'follow_end'],
571
+ );
572
+ const humanShot = await runCli(['dev', 'screenshot'], gameDirectory);
573
+ assert.equal(humanShot.status, 0, humanShot.stderr || humanShot.stdout);
574
+ assert.match(humanShot.stdout, /default\.jpeg/);
575
+ assert.ok(fs.existsSync(path.join(screenshotsDirectory, 'default.jpeg')));
576
+ const jsonShot = await runCli(
577
+ [
578
+ 'dev',
579
+ 'screenshot',
580
+ '--output',
581
+ path.join(root, 'alias.jpeg'),
582
+ '--format',
583
+ 'json',
584
+ ],
585
+ gameDirectory,
586
+ );
587
+ assert.equal(jsonShot.status, 0, jsonShot.stderr || jsonShot.stdout);
588
+ assert.equal(JSON.parse(jsonShot.stdout).mime, 'image/jpeg');
589
+ const noSession = await runCli(['dev', 'logs', '--format', 'json'], root);
590
+ assert.equal(noSession.status, 1);
591
+ assert.equal(
592
+ JSON.parse(noSession.stdout).errorCode,
593
+ 'DEV_SESSION_NOT_RUNNING',
594
+ );
595
+ } finally {
596
+ await new Promise(resolve => server.close(resolve));
597
+ fs.rmSync(root, { recursive: true, force: true });
598
+ }
599
+
600
+ console.log('client preview control process smoke passed');
601
+ }
602
+
603
+ run().catch(error => {
604
+ console.error(error);
605
+ process.exitCode = 1;
606
+ });
@@ -0,0 +1,67 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const { spawn } = require('node:child_process');
6
+
7
+ const delay = milliseconds =>
8
+ new Promise(resolve => setTimeout(resolve, milliseconds));
9
+
10
+ async function run() {
11
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-default-dev-'));
12
+ const gameDirectory = path.join(root, 'game');
13
+ fs.mkdirSync(gameDirectory);
14
+ fs.writeFileSync(path.join(gameDirectory, 'game.js'), 'console.log("game");\n');
15
+ fs.writeFileSync(path.join(gameDirectory, 'game.json'), '{}\n');
16
+
17
+ let output = '';
18
+ const child = spawn(
19
+ process.execPath,
20
+ [
21
+ path.resolve(__dirname, '../dist/index.js'),
22
+ 'dev',
23
+ '--client-key',
24
+ 'smoke-client',
25
+ ],
26
+ {
27
+ cwd: gameDirectory,
28
+ env: { ...process.env, TTMG_DEV_NO_OPEN: '1' },
29
+ stdio: ['ignore', 'pipe', 'pipe'],
30
+ },
31
+ );
32
+ child.stdout.on('data', chunk => {
33
+ output += chunk;
34
+ });
35
+ child.stderr.on('data', chunk => {
36
+ output += chunk;
37
+ });
38
+
39
+ try {
40
+ const deadline = Date.now() + 20000;
41
+ while (
42
+ !output.includes('Game resources compiled successfully') &&
43
+ !output.includes('游戏资源编译成功') &&
44
+ Date.now() < deadline
45
+ ) {
46
+ if (child.exitCode !== null) {
47
+ throw new Error(`Default ttmg dev exited unexpectedly:\n${output}`);
48
+ }
49
+ await delay(100);
50
+ }
51
+ assert.match(output, /Game resources compiled successfully|游戏资源编译成功/);
52
+ assert.equal(fs.existsSync(path.join(gameDirectory, '.ttmg')), false);
53
+ } finally {
54
+ if (child.exitCode === null) {
55
+ child.kill('SIGTERM');
56
+ await new Promise(resolve => child.once('close', resolve));
57
+ }
58
+ fs.rmSync(root, { recursive: true, force: true });
59
+ }
60
+
61
+ console.log('default dev compatibility smoke passed');
62
+ }
63
+
64
+ run().catch(error => {
65
+ console.error(error);
66
+ process.exitCode = 1;
67
+ });