@ttmg/cli 0.4.6-vibe-beta.7 → 0.4.6-vibe-beta.8
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.
- package/README.md +426 -22
- package/dist/index.js +3413 -181
- package/dist/index.js.map +1 -1
- package/dist/package.json +11 -9
- package/dist/scripts/acceptance-fixture.test.js +33 -0
- package/dist/scripts/dev-capture-smoke.js +76 -0
- package/dist/scripts/dev-client-preview-control-smoke.js +293 -9
- package/dist/scripts/dev-default-smoke.js +22 -1
- package/dist/scripts/dev-workflow-smoke.js +260 -0
- package/dist/scripts/prepare-client-preview-acceptance.js +65 -0
- package/dist/scripts/verify-agent-contract.js +199 -2
- package/dist/scripts/vibe-auth-open-test-preload.cjs +11 -0
- package/dist/scripts/vibe-upload-smoke.js +177 -2
- package/dist/scripts/wasm-smoke.js +53 -0
- package/dist/scripts/wasm-worker.js +63 -0
- package/package.json +11 -9
- package/vibe-upload.md +107 -3
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttmg/cli",
|
|
3
|
-
"version": "0.4.6-vibe-beta.
|
|
3
|
+
"version": "0.4.6-vibe-beta.8",
|
|
4
4
|
"description": "TikTok Mini Game Command Line Tool",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"bin": {
|
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
"test:auth": "node scripts/run-ts-test.js src/commands/common/auth.test.ts",
|
|
32
32
|
"test:capabilities": "node scripts/run-ts-test.js src/commands/common/capabilities.test.ts",
|
|
33
33
|
"test:test-account": "node scripts/run-ts-test.js src/commands/common/testAccount.test.ts",
|
|
34
|
-
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts",
|
|
35
|
-
"test:
|
|
34
|
+
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts && npm run test:packaging-compatibility",
|
|
35
|
+
"test:packaging-compatibility": "node scripts/run-ts-test.js src/commands/native/dev/packagingCompatibility.test.ts",
|
|
36
|
+
"test:dev-session-command": "node scripts/run-ts-test.js src/commands/native/dev/session.test.ts && node scripts/run-ts-test.js src/commands/native/dev/projectSession.test.ts && node scripts/run-ts-test.js src/commands/native/dev/scanPage.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/logArchive.test.ts && node scripts/run-ts-test.js src/commands/native/dev/capture.test.ts",
|
|
36
37
|
"test:dev-runtime-event": "node scripts/run-ts-test.js src/commands/native/dev/utils/scoket.test.ts",
|
|
37
38
|
"test:dev-runtime-client": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtime.test.ts",
|
|
38
|
-
"test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts",
|
|
39
|
+
"test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/previewWorkflow.test.ts",
|
|
39
40
|
"test:dev-runtime-session-follow": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtimeSessionFollow.test.ts",
|
|
40
41
|
"test:dev-client-preview-orchestrator": "node scripts/run-ts-test.js src/commands/native/dev/runtime/clientPreviewOrchestrator.test.ts",
|
|
41
42
|
"test:dev-watch-change": "node scripts/run-ts-test.js src/commands/native/dev/utils/watchChange.test.ts",
|
|
@@ -46,9 +47,10 @@
|
|
|
46
47
|
"test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
|
|
47
48
|
"test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
|
|
48
49
|
"test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
|
|
49
|
-
"test:
|
|
50
|
-
"test:agent-contract
|
|
51
|
-
"
|
|
50
|
+
"test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
|
|
51
|
+
"test:agent-contract": "npm run test:login && npm run test:auth && npm run test:capabilities && npm run test:test-account && npm run test:upload-preview && npm run test:preview-page && npm run test:check && npm run test:check-ai && npm run test:build-structured && npm run test:doctor && npm run test:wasm && npm run test:dev-session && npm run test:dev-session-command && npm run test:dev-runtime-event && npm run test:dev-runtime-client && npm run test:dev-session-control && npm run test:dev-runtime-session-follow && npm run test:dev-client-preview-orchestrator && npm run test:dev-watch-change && npm run test:network && npm run test:vibe && npx tsc --noEmit && npm run build && npm run test:vibe-process && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
|
|
52
|
+
"test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process && npm run test:dev-client-preview-control-process && node scripts/dev-workflow-smoke.js",
|
|
53
|
+
"publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag vibe-beta",
|
|
52
54
|
"setup": "node scripts/setup.js",
|
|
53
55
|
"reset:local-state": "node scripts/resetLocalState.js",
|
|
54
56
|
"dev:link": "npm run build && npm link",
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
],
|
|
61
63
|
"author": "zhanghongyang.mocha",
|
|
62
64
|
"dependencies": {
|
|
63
|
-
"@anrans001/ttmg-wasmtool": "^0.3.
|
|
65
|
+
"@anrans001/ttmg-wasmtool": "^0.3.3",
|
|
64
66
|
"acorn": "^8.15.0",
|
|
65
67
|
"archiver": "^7.0.1",
|
|
66
68
|
"axios": "^1.11.0",
|
|
@@ -93,7 +95,7 @@
|
|
|
93
95
|
"qs": "6.12.1",
|
|
94
96
|
"semver": "^7.7.2",
|
|
95
97
|
"socks-proxy-agent": "^9.0.0",
|
|
96
|
-
"ttmg-pack": "0.4.13
|
|
98
|
+
"ttmg-pack": "0.4.13",
|
|
97
99
|
"ws": "^8.18.3"
|
|
98
100
|
},
|
|
99
101
|
"devDependencies": {
|
|
@@ -0,0 +1,33 @@
|
|
|
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 vm = require('node:vm');
|
|
6
|
+
const { test } = require('node:test');
|
|
7
|
+
const { prepareFixture } = require('./prepare-client-preview-acceptance');
|
|
8
|
+
|
|
9
|
+
test('two fixture rounds have unique correlated logs/screens and never start a client', t => {
|
|
10
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-acceptance-test-'));
|
|
11
|
+
t.after(() => fs.rmSync(temporary, { recursive: true, force: true }));
|
|
12
|
+
const root = path.join(temporary, 'run');
|
|
13
|
+
const result = prepareFixture(root);
|
|
14
|
+
assert.equal(result.deviceVerified, false);
|
|
15
|
+
assert.equal(result.clientStarted, false);
|
|
16
|
+
assert.throws(() => prepareFixture(root), /EEXIST/);
|
|
17
|
+
assert.notEqual(prepareFixture(path.join(temporary, 'other')).runId, result.runId);
|
|
18
|
+
for (const round of result.rounds) {
|
|
19
|
+
const logs = [], text = [];
|
|
20
|
+
const context = { fillRect() {}, fillText(value) { text.push(value); } };
|
|
21
|
+
vm.runInNewContext(fs.readFileSync(path.join(round.source, 'game.js'), 'utf8'), {
|
|
22
|
+
tt: { createCanvas: () => ({ getContext: () => context }),
|
|
23
|
+
getSystemInfoSync: () => ({ windowWidth: 360, windowHeight: 640 }), onShow() {} },
|
|
24
|
+
console: Object.fromEntries(['info', 'warn', 'error'].map(level => [level, value => logs.push([level, value])])),
|
|
25
|
+
});
|
|
26
|
+
assert.deepEqual(logs.map(x => x[0]), round.severities);
|
|
27
|
+
assert.ok(logs.every(x => x[1].startsWith(round.marker)));
|
|
28
|
+
assert.equal(text[0], 'ROUND ' + round.round);
|
|
29
|
+
assert.equal(text.slice(1).join(''), result.runId);
|
|
30
|
+
}
|
|
31
|
+
assert.equal(fs.readFileSync(path.join(result.project, 'game.js'), 'utf8'),
|
|
32
|
+
fs.readFileSync(path.join(root, 'round-a', 'game.js'), 'utf8'));
|
|
33
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
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 net = require('node:net');
|
|
6
|
+
const { spawn } = require('node:child_process');
|
|
7
|
+
const entry = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, '../dist/index.js');
|
|
8
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(process.platform === 'win32' ? os.tmpdir() : '/tmp', 'ttmg-cap-')));
|
|
9
|
+
const id = 'dev_capture_test';
|
|
10
|
+
const directory = path.join(root, '.ttmg', 'sessions', id);
|
|
11
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
12
|
+
fs.writeFileSync(path.join(root, 'game.json'), '{}');
|
|
13
|
+
const address = process.platform === 'win32'
|
|
14
|
+
? { transport: 'named-pipe', path: `\\\\.\\pipe\\ttmg-${id}` }
|
|
15
|
+
: { transport: 'unix', path: path.join(directory, 'ipc') };
|
|
16
|
+
fs.writeFileSync(path.join(directory, 'manifest.json'), JSON.stringify({
|
|
17
|
+
schemaVersion: '1', sessionId: id, mode: 'client-preview', status: 'ready',
|
|
18
|
+
project: { directory: root, clientKey: 'fixture' }, process: { pid: process.pid },
|
|
19
|
+
control: address, runtime: { runtimeSessionId: 'runtime_fixture', sessionStartCursor: '0' },
|
|
20
|
+
}));
|
|
21
|
+
const rows = Array.from({ length: 1201 }, (_, i) => ({ sequence: String(i + 1), severity: 'info', message: `fixture ${i}` }));
|
|
22
|
+
const server = net.createServer(socket => {
|
|
23
|
+
let buffer = '';
|
|
24
|
+
socket.on('data', data => {
|
|
25
|
+
buffer += data;
|
|
26
|
+
if (!buffer.includes('\n')) return;
|
|
27
|
+
const request = JSON.parse(buffer.trim());
|
|
28
|
+
const params = request.params || {};
|
|
29
|
+
let result = { runtimeSessionId: 'runtime_fixture' };
|
|
30
|
+
if (request.operation === 'logs.snapshot') {
|
|
31
|
+
const until = params.until || '1201';
|
|
32
|
+
const eligible = rows.filter(x => BigInt(x.sequence) > BigInt(params.after || '0') && BigInt(x.sequence) <= BigInt(until));
|
|
33
|
+
const records = eligible.slice(0, params.limit);
|
|
34
|
+
result = { ...result, records, snapshotCursor: until, oldestCursor: '1',
|
|
35
|
+
nextCursor: records.length ? records[records.length - 1].sequence : until,
|
|
36
|
+
cursorExpired: false, hasMore: eligible.length > records.length };
|
|
37
|
+
} else if (request.operation === 'screenshot.capture') {
|
|
38
|
+
fs.writeFileSync(params.output, 'fixture screenshot');
|
|
39
|
+
result = { ...result, path: params.output, mime: 'image/jpeg', width: 10, height: 20, captureTime: '123' };
|
|
40
|
+
}
|
|
41
|
+
socket.end(JSON.stringify({ requestId: request.requestId, type: 'result', result }) + '\n');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
function run(args) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const child = spawn(process.execPath, [entry, ...args, '--format', 'json'], {
|
|
47
|
+
cwd: root, env: { ...process.env, TTMGRC_PATH: path.join(root, 'rc.json') }, stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
let stdout = '', stderr = '';
|
|
50
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 15000);
|
|
51
|
+
child.stdout.on('data', x => { stdout += x; });child.stderr.on('data', x => { stderr += x; });
|
|
52
|
+
child.on('error', reject);
|
|
53
|
+
child.on('close', code => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
try { assert.equal(code, 0, stderr + stdout);assert.equal(stdout.trim().split('\n').length, 1);resolve(JSON.parse(stdout)); }
|
|
56
|
+
catch (error) { reject(error); }
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
(async () => {
|
|
61
|
+
try {
|
|
62
|
+
await new Promise((resolve, reject) => { server.once('error', reject);server.listen(address.path, resolve); });
|
|
63
|
+
const legacy = await run(['dev', 'logs']);
|
|
64
|
+
assert.equal(legacy.records.length, 200);
|
|
65
|
+
assert.equal(legacy.cursor.hasMore, true);
|
|
66
|
+
const capture = await run(['dev', 'capture']);
|
|
67
|
+
assert.equal(capture.logs.records, 1201);assert.equal(capture.logs.complete, true);
|
|
68
|
+
assert.deepEqual(fs.readFileSync(capture.logs.path, 'utf8').trim().split('\n').map(JSON.parse), rows);
|
|
69
|
+
assert.equal(fs.existsSync(capture.screenshot.path), true);
|
|
70
|
+
assert.equal(capture.runtimeSessionId, 'runtime_fixture');
|
|
71
|
+
assert.equal(capture.platformWrite, false);
|
|
72
|
+
console.log('capture process passed: 1201 logs, screenshot path, session discovery, unchanged paged logs; local IPC fixture only');
|
|
73
|
+
} finally {
|
|
74
|
+
await new Promise(resolve => server.close(resolve));fs.rmSync(root, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
})().catch(error => { console.error(error);process.exitCode = 1; });
|
|
@@ -5,27 +5,67 @@ const os = require('node:os');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { spawn } = require('node:child_process');
|
|
7
7
|
|
|
8
|
-
function runCli(args, cwd) {
|
|
8
|
+
function runCli(args, cwd, inspectScanPage) {
|
|
9
9
|
return new Promise((resolve, reject) => {
|
|
10
10
|
const child = spawn(
|
|
11
11
|
process.execPath,
|
|
12
12
|
[path.resolve(__dirname, '../dist/index.js'), ...args],
|
|
13
13
|
{
|
|
14
14
|
cwd,
|
|
15
|
-
env: {
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
NO_COLOR: '1',
|
|
18
|
+
FORCE_COLOR: '0',
|
|
19
|
+
TTMG_DEV_NO_OPEN: '1',
|
|
20
|
+
},
|
|
16
21
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
22
|
},
|
|
18
23
|
);
|
|
19
24
|
let stdout = '';
|
|
20
25
|
let stderr = '';
|
|
26
|
+
let inspection = Promise.resolve();
|
|
27
|
+
let inspectionStarted = false;
|
|
28
|
+
let inspectionError;
|
|
29
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 20000);
|
|
21
30
|
child.stdout.on('data', chunk => {
|
|
22
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
|
+
}
|
|
23
48
|
});
|
|
24
49
|
child.stderr.on('data', chunk => {
|
|
25
50
|
stderr += chunk;
|
|
26
51
|
});
|
|
27
|
-
child.once('error',
|
|
28
|
-
|
|
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
|
+
});
|
|
29
69
|
});
|
|
30
70
|
}
|
|
31
71
|
|
|
@@ -92,6 +132,15 @@ async function run() {
|
|
|
92
132
|
buffer += chunk.toString();
|
|
93
133
|
if (!buffer.includes('\n')) return;
|
|
94
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
|
+
}
|
|
95
144
|
if (request.operation === 'logs.snapshot') {
|
|
96
145
|
write(socket, {
|
|
97
146
|
requestId: request.requestId,
|
|
@@ -157,15 +206,31 @@ async function run() {
|
|
|
157
206
|
return;
|
|
158
207
|
}
|
|
159
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
|
+
}
|
|
160
222
|
assert.equal(request.params.format, 'jpeg');
|
|
161
223
|
assert.equal(request.params.quality, 0.8);
|
|
162
|
-
const output =
|
|
224
|
+
const output =
|
|
225
|
+
request.params.output ||
|
|
226
|
+
path.join(screenshotsDirectory, 'default.jpeg');
|
|
163
227
|
fs.writeFileSync(output, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
164
228
|
write(socket, {
|
|
165
229
|
requestId: request.requestId,
|
|
166
230
|
type: 'result',
|
|
167
231
|
result: {
|
|
168
232
|
path: output,
|
|
233
|
+
runtimeSessionId: 'runtime-control-smoke',
|
|
169
234
|
mime: 'image/jpeg',
|
|
170
235
|
size: 4,
|
|
171
236
|
width: 1,
|
|
@@ -190,10 +255,53 @@ async function run() {
|
|
|
190
255
|
'--events',
|
|
191
256
|
'ndjson',
|
|
192
257
|
'--no-open',
|
|
258
|
+
'--record-logs',
|
|
193
259
|
'--timeout',
|
|
194
|
-
'
|
|
260
|
+
'3',
|
|
195
261
|
],
|
|
196
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
|
+
},
|
|
197
305
|
);
|
|
198
306
|
assert.equal(previewRun.status, 2, previewRun.stderr || previewRun.stdout);
|
|
199
307
|
const previewEvents = previewRun.stdout
|
|
@@ -203,16 +311,49 @@ async function run() {
|
|
|
203
311
|
.map(JSON.parse);
|
|
204
312
|
const readyForScan = previewEvents.find(
|
|
205
313
|
event =>
|
|
206
|
-
event.type === 'lifecycle' &&
|
|
207
|
-
event.payload?.state === 'READY_FOR_SCAN',
|
|
314
|
+
event.type === 'lifecycle' && event.payload?.state === 'READY_FOR_SCAN',
|
|
208
315
|
);
|
|
209
316
|
assert.ok(readyForScan, previewRun.stderr || previewRun.stdout);
|
|
210
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
|
+
);
|
|
211
328
|
const previewManifest = JSON.parse(
|
|
212
329
|
fs.readFileSync(readyForScan.payload.manifest, 'utf8'),
|
|
213
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);
|
|
214
352
|
if (previewManifest.control?.transport === 'unix') {
|
|
215
|
-
assert.equal(
|
|
353
|
+
assert.equal(
|
|
354
|
+
Buffer.byteLength(previewManifest.control.path) <= 100,
|
|
355
|
+
true,
|
|
356
|
+
);
|
|
216
357
|
}
|
|
217
358
|
assert.match(previewRun.stderr, /QR image:/);
|
|
218
359
|
assert.equal(
|
|
@@ -224,6 +365,39 @@ async function run() {
|
|
|
224
365
|
'session evidence must not trigger an endless recompilation loop',
|
|
225
366
|
);
|
|
226
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
|
+
|
|
227
401
|
await new Promise((resolve, reject) => {
|
|
228
402
|
server.once('error', reject);
|
|
229
403
|
server.listen(control.path, resolve);
|
|
@@ -305,9 +479,119 @@ async function run() {
|
|
|
305
479
|
assert.equal(screenshotLines.length, 1);
|
|
306
480
|
const screenshot = JSON.parse(screenshotLines[0]);
|
|
307
481
|
assert.equal(screenshot.path, screenshotOutput);
|
|
482
|
+
assert.equal(screenshot.runtimeSessionId, 'runtime-control-smoke');
|
|
308
483
|
assert.equal(screenshot.mime, 'image/jpeg');
|
|
309
484
|
assert.equal('data' in screenshot, false);
|
|
310
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
|
+
);
|
|
311
595
|
} finally {
|
|
312
596
|
await new Promise(resolve => server.close(resolve));
|
|
313
597
|
fs.rmSync(root, { recursive: true, force: true });
|
|
@@ -13,6 +13,10 @@ async function run() {
|
|
|
13
13
|
fs.mkdirSync(gameDirectory);
|
|
14
14
|
fs.writeFileSync(path.join(gameDirectory, 'game.js'), 'console.log("game");\n');
|
|
15
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
|
+
}
|
|
16
20
|
|
|
17
21
|
let output = '';
|
|
18
22
|
const child = spawn(
|
|
@@ -49,7 +53,24 @@ async function run() {
|
|
|
49
53
|
await delay(100);
|
|
50
54
|
}
|
|
51
55
|
assert.match(output, /Game resources compiled successfully|游戏资源编译成功/);
|
|
52
|
-
|
|
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
|
+
}
|
|
53
74
|
} finally {
|
|
54
75
|
if (child.exitCode === null) {
|
|
55
76
|
child.kill('SIGTERM');
|