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

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 (83) hide show
  1. package/README.md +659 -2
  2. package/dist/index.js +22632 -5993
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +37 -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 +88 -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 +448 -0
  65. package/dist/scripts/vibe-auth-open-test-preload.cjs +11 -0
  66. package/dist/scripts/vibe-ppe-test-preload.cjs +23 -0
  67. package/dist/scripts/vibe-upload-demo.js +72 -0
  68. package/dist/scripts/vibe-upload-smoke.js +466 -0
  69. package/dist/scripts/wasm-smoke.js +53 -0
  70. package/dist/scripts/wasm-worker.js +63 -0
  71. package/dist/scripts/worker.js +28 -22
  72. package/package.json +37 -4
  73. package/CHANGELOG.md +0 -277
  74. package/dist/public/assets/Detail-rVxbEDgh.js.br +0 -0
  75. package/dist/public/assets/baseForm-kBTxn8h8.js.br +0 -0
  76. package/dist/public/assets/index-Bjn0ctEd.js.br +0 -0
  77. package/dist/public/assets/index-BxokHznc.js.br +0 -0
  78. package/dist/public/assets/index-Cl0_vMWE.js.br +0 -0
  79. package/dist/public/assets/index-DHdJsD10.js.br +0 -0
  80. package/dist/public/assets/index-DIcz5ciL.js.br +0 -0
  81. package/dist/public/assets/index-O_L1tU2b.js.br +0 -0
  82. package/dist/public/assets/index-aSl6Wdrp.js.br +0 -0
  83. 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,88 @@
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
+ for (const directory of ['.ttmg', '.ttmg-assets', 'assets/.ttmg']) {
17
+ fs.mkdirSync(path.join(gameDirectory, directory), { recursive: true });
18
+ fs.writeFileSync(path.join(gameDirectory, directory, 'data.txt'), 'before');
19
+ }
20
+
21
+ let output = '';
22
+ const child = spawn(
23
+ process.execPath,
24
+ [
25
+ path.resolve(__dirname, '../dist/index.js'),
26
+ 'dev',
27
+ '--client-key',
28
+ 'smoke-client',
29
+ ],
30
+ {
31
+ cwd: gameDirectory,
32
+ env: { ...process.env, TTMG_DEV_NO_OPEN: '1' },
33
+ stdio: ['ignore', 'pipe', 'pipe'],
34
+ },
35
+ );
36
+ child.stdout.on('data', chunk => {
37
+ output += chunk;
38
+ });
39
+ child.stderr.on('data', chunk => {
40
+ output += chunk;
41
+ });
42
+
43
+ try {
44
+ const deadline = Date.now() + 20000;
45
+ while (
46
+ !output.includes('Game resources compiled successfully') &&
47
+ !output.includes('游戏资源编译成功') &&
48
+ Date.now() < deadline
49
+ ) {
50
+ if (child.exitCode !== null) {
51
+ throw new Error(`Default ttmg dev exited unexpectedly:\n${output}`);
52
+ }
53
+ await delay(100);
54
+ }
55
+ assert.match(output, /Game resources compiled successfully|游戏资源编译成功/);
56
+ // Ordinary dev must not create an Agent session. Existing evidence must
57
+ // not trigger reloads, while similarly named runtime assets still do.
58
+ assert.equal(fs.existsSync(path.join(gameDirectory, '.ttmg/sessions')), false);
59
+ const compileCount = () => (output.match(/Game resources compiled successfully|游戏资源编译成功/g) || []).length;
60
+ const initialCount = compileCount();
61
+ fs.writeFileSync(path.join(gameDirectory, '.ttmg/data.txt'), 'new log');
62
+ await delay(4500);
63
+ assert.equal(compileCount(), initialCount, 'session writes triggered a rebuild');
64
+ for (const file of ['.ttmg-assets/data.txt', 'assets/.ttmg/data.txt']) {
65
+ const before = compileCount();
66
+ fs.writeFileSync(path.join(gameDirectory, file), 'updated runtime asset');
67
+ const deadline = Date.now() + 15000;
68
+ while (compileCount() === before && Date.now() < deadline) {
69
+ if (child.exitCode !== null) throw new Error(`Default dev exited while watching ${file}:\n${output}`);
70
+ await delay(100);
71
+ }
72
+ assert.ok(compileCount() > before, `${file} did not trigger a rebuild`);
73
+ }
74
+ } finally {
75
+ if (child.exitCode === null) {
76
+ child.kill('SIGTERM');
77
+ await new Promise(resolve => child.once('close', resolve));
78
+ }
79
+ fs.rmSync(root, { recursive: true, force: true });
80
+ }
81
+
82
+ console.log('default dev compatibility smoke passed');
83
+ }
84
+
85
+ run().catch(error => {
86
+ console.error(error);
87
+ process.exitCode = 1;
88
+ });