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

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/dist/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-agent-beta.2",
3
+ "version": "0.4.6-agent-beta.3",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
7
7
  "ttmg": "dist/index.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "vibe-upload.md"
11
12
  ],
12
13
  "publishConfig": {
13
14
  "access": "public",
@@ -15,7 +16,8 @@
15
16
  },
16
17
  "scripts": {
17
18
  "demo:vibe": "npm run build && node scripts/vibe-upload-demo.js",
18
- "test:vibe": "node scripts/run-ts-test.js src/commands/common/vibe/vibe.test.ts",
19
+ "test:game-categories": "node scripts/run-ts-test.js src/commands/common/game/categories.test.ts",
20
+ "test:vibe": "npm run test:game-categories && node scripts/run-ts-test.js src/commands/common/vibe/vibe.test.ts",
19
21
  "test:vibe-process": "node scripts/vibe-upload-smoke.js",
20
22
  "prepack": "npm run build && npm run verify:dist-version && npm run verify:agent-contract",
21
23
  "build": "node scripts/build.js",
@@ -46,7 +48,7 @@
46
48
  "test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
47
49
  "test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
48
50
  "test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
49
- "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
51
+ "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/run-ts-test.js src/commands/native/wasm/collect.test.ts && node scripts/acceptance-fixture.test.js",
50
52
  "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",
51
53
  "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",
52
54
  "publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag beta",
@@ -126,6 +126,18 @@ const capabilitiesResult = runCli(['capabilities', '--format', 'json']);
126
126
  assertExit(capabilitiesResult, 0, 'capabilities');
127
127
  const capabilities = parseSingleJson(capabilitiesResult, 'capabilities');
128
128
  assert.equal(capabilities.command, 'capabilities');
129
+ const gameCategories = runCli(['game', 'categories', '--format', 'json']);
130
+ assertExit(gameCategories, 0, 'game categories');
131
+ const categoryCatalog = parseSingleJson(gameCategories, 'game categories');
132
+ assert.equal(categoryCatalog.platformWrite, false);
133
+ assert.equal(categoryCatalog.categoryCount, 12);
134
+ assert.equal(categoryCatalog.subcategoryCount, 114);
135
+ assert.equal(capabilities.commands.gameCategories.catalogVersion, categoryCatalog.catalogVersion);
136
+ assert.match(capabilities.commands.vibeUpload.command, /--vibe --web/);
137
+ assert.equal(capabilities.commands.vibeUpload.skillWorkflow.keepUploadProcessRunning, true);
138
+ assert.equal(capabilities.commands.vibeUpload.skillWorkflow.realUploadSuccess.simulation, false);
139
+ assert.equal(capabilities.commands.vibeUpload.appInfoFields.defaultedByCli.includes('privacy_policy'), true);
140
+ assert.equal(packageJson.files.includes('vibe-upload.md'), true);
129
141
  assert.equal(capabilities.structuredOutput.stdoutProtocolOnly, true);
130
142
  assert.equal(capabilities.commands.check.platformWrite, false);
131
143
  assert.equal(capabilities.commands.check.ai.option, '--ai');
@@ -385,7 +397,14 @@ assert.notEqual(
385
397
  );
386
398
 
387
399
  const uploadHelp = runCli(['upload', '--help']);
400
+ assert.doesNotMatch(uploadHelp.stdout, /--app-info/);
401
+ assert.match(uploadHelp.stdout.replace(/\s+/g, ' '), /only title and icon|只收集标题和头像/);
388
402
  assertExit(uploadHelp, 0, 'upload help');
403
+ assert.match(uploadHelp.stdout, /--vibe --web --dir/);
404
+ assert.match(uploadHelp.stdout, /authorizationOpenCommand/);
405
+ assert.match(uploadHelp.stdout, /simulation=false/);
406
+ assert.match(uploadHelp.stdout, /--category <category>/);
407
+ assert.match(uploadHelp.stdout, /--subcategory <subcategory>/);
389
408
  assert.match(uploadHelp.stdout, /--preview\b/);
390
409
  assert.match(uploadHelp.stdout, /--serve-preview-page\b/);
391
410
  assert.match(uploadHelp.stdout, /--no-open\b/);
@@ -432,6 +451,17 @@ const wasmSmoke = spawnSync(process.execPath, [path.join(cliRoot, 'scripts/wasm-
432
451
  });
433
452
  assertExit(wasmSmoke, 0, 'Wasm built-artifact process smoke');
434
453
  console.log(wasmSmoke.stdout.trim());
454
+ for (const operation of ['start', 'export']) {
455
+ assertExit(runWithoutWasmBinding(['wasm', 'collect', operation, '--help']), 0, `collect ${operation} help without binding`);
456
+ const invalid = runCli(['wasm', 'collect', operation, '--format', 'json']);
457
+ assertExit(invalid, 1, `collect ${operation} missing input`);
458
+ assert.equal(parseSingleJson(invalid, `collect ${operation}`).errorCode, 'WASM_COLLECT_INVALID_OPTIONS');
459
+ }
460
+ const collectSmoke = spawnSync(process.execPath, [path.join(cliRoot, 'scripts/wasm-collect-smoke.js')], {
461
+ cwd: cliRoot, encoding: 'utf8', timeout: 60000,
462
+ });
463
+ assertExit(collectSmoke, 0, 'Wasm collection built-artifact process smoke');
464
+ console.log(collectSmoke.stdout.trim());
435
465
  const captureHelp = runCli(['dev', 'capture', '--help']);
436
466
  assertExit(captureHelp, 0, 'capture help');
437
467
  assert.match(captureHelp.stdout, /--timeout/);
@@ -3,18 +3,32 @@
3
3
  const assert = require('node:assert/strict');
4
4
  const http = require('node:http');
5
5
  const https = require('node:https');
6
+ const net = require('node:net');
6
7
  const port = Number(process.env.TTMG_VIBE_TEST_PORT);
7
8
  assert.ok(Number.isInteger(port) && port > 0 && port < 65536);
8
9
  https.request = (options, callback) => {
9
10
  assert.equal(typeof options, 'object');
10
11
  assert.equal(options.hostname, 'developers.tiktok.com');
11
12
  assert.equal(options.protocol, 'https:');
12
- assert.match(options.path, /^\/bff\/app_info\/minis_import\/(create_minis_import_task_for_agent|get_minis_import_task_status_for_agent)\//);
13
- return http.request({
13
+ assert.match(options.path, /^\/bff\/app_info\/minis_import\/(create_minis_import_task_for_agent|get_minis_import_task_status_for_agent|upload_minis_import_app_icon_for_agent|submit_minis_import_info_for_agent|upload_minis_import_package_for_agent)\//);
14
+ const request = http.request({
14
15
  ...options,
15
16
  protocol: 'http:', hostname: '127.0.0.1', host: undefined, port,
16
17
  agent: undefined, servername: undefined,
17
18
  }, callback);
19
+ if (process.env.TTMG_VIBE_TEST_RETAIN_UPLOAD_SOCKET === '1' &&
20
+ /upload_minis_import_(app_icon|package)_for_agent/.test(options.path)) {
21
+ // Reproduce the live gateway's completed response with a retained upload
22
+ // socket. The socket is real but loopback-only; operation cleanup must close it.
23
+ assert.ok(options.agent && options.agent !== https.globalAgent);
24
+ request.once('response', response => response.once('end', () => {
25
+ const socket = net.connect({host: '127.0.0.1', port});
26
+ const key = 'retained-vibe-test-sockets';
27
+ (options.agent.sockets[key] ||= []).push(socket);
28
+ socket.on('error', () => {});
29
+ }));
30
+ }
31
+ return request;
18
32
  };
19
33
  https.get = (options, callback) => {
20
34
  const request = https.request(options, callback);
@@ -27,6 +27,10 @@ const children = new Set();
27
27
  let fixtureServer;
28
28
  const requests = [];
29
29
  let failStatus = false;
30
+ let webMode = false;
31
+ let webPolls = 0;
32
+ let webFailureEndpoint;
33
+ let expectedWebCategory = { category: 'Other', subcategory: 'Other' };
30
34
  const env = {
31
35
  ...process.env,
32
36
  HOME: temp,
@@ -96,7 +100,16 @@ function events(result) {
96
100
  return result.stdout
97
101
  .trim()
98
102
  .split('\n')
99
- .map(line => JSON.parse(line));
103
+ .map(line => {
104
+ const event = JSON.parse(line);
105
+ if (event.mode === 'vibe') {
106
+ assert.deepEqual(event.inputRequirements.requiredUserInputs, ['title', 'icon']);
107
+ assert.equal(event.inputRequirements.requestAdditionalMetadata, false);
108
+ assert.equal(event.inputRequirements.additionalMetadataSource, 'cli-defaults');
109
+ assert.equal(event.inputRequirements.defaultMetadataProfile, 'ppe-trial');
110
+ }
111
+ return event;
112
+ });
100
113
  }
101
114
  async function main() {
102
115
  // Exercise the built worker IPC and new command without creating a platform task.
@@ -150,13 +163,20 @@ async function main() {
150
163
  assert.equal(fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').length, 2);
151
164
  authWorker.kill();
152
165
  children.delete(authWorker);
153
- fixtureServer = http.createServer((req, res) => {
166
+ fixtureServer = http.createServer(async (req, res) => {
154
167
  const url = new URL(req.url, 'http://127.0.0.1');
155
168
  requests.push({ path: url.pathname, method: req.method, headers: req.headers });
156
- req.resume();
169
+ const chunks = [];
170
+ for await (const chunk of req) chunks.push(chunk);
171
+ const body = Buffer.concat(chunks);
157
172
  const create = url.pathname.endsWith('/create_minis_import_task_for_agent/');
158
173
  const status = url.pathname.endsWith('/get_minis_import_task_status_for_agent/');
159
- assert.ok(create || status, 'Unexpected real upload endpoint');
174
+ const iconUpload = url.pathname.endsWith('/upload_minis_import_app_icon_for_agent/');
175
+ const infoUpload = url.pathname.endsWith('/submit_minis_import_info_for_agent/');
176
+ const packageUpload = url.pathname.endsWith('/upload_minis_import_package_for_agent/');
177
+ if (create) webPolls = 0;
178
+ if (status && webMode) webPolls++;
179
+ assert.ok(create || status || (webMode && (iconUpload || infoUpload || packageUpload)), 'Mock must not call material-upload endpoints');
160
180
  assert.equal(req.headers['x-use-ppe'], '1');
161
181
  assert.equal(req.headers['x-tt-env'], 'ppe_op_vc');
162
182
  assert.equal(req.headers.cookie, undefined);
@@ -164,13 +184,41 @@ async function main() {
164
184
  assert.equal(url.searchParams.get('task_id'), '7684370037737553941');
165
185
  assert.equal(url.searchParams.get('import_token'), 'process-test-secret');
166
186
  }
187
+ if (iconUpload || infoUpload || packageUpload) {
188
+ assert.equal(webPolls, 5, 'Do not upload before server readiness');
189
+ assert.equal(req.method, 'POST');
190
+ if (infoUpload) {
191
+ assert.match(body.toString(), /"task_id":7684370037737553941/);
192
+ const info = JSON.parse(body.toString());
193
+ assert.equal(info.import_token, 'process-test-secret');
194
+ assert.equal(info.app_info.icon_uri, 'web-icon-receipt');
195
+ assert.equal(info.app_info.app_name, input[input.indexOf('--title') + 1]);
196
+ assert.deepEqual(info.app_info, {
197
+ description: 'DevTool PPE integration test fixture. Not a production game.',
198
+ ...expectedWebCategory,
199
+ terms_of_service: 'https://www.tiktok.com',
200
+ privacy_policy: 'https://www.tiktok.com',
201
+ copyright_confirmation: true, contact_email: 'devtool-test@example.invalid',
202
+ app_name: input[input.indexOf('--title') + 1], icon_uri: 'web-icon-receipt',
203
+ }, 'CLI must send complete PPE trial defaults without asking the user for them');
204
+ } else {
205
+ assert.match(req.headers['content-type'], /^multipart\/form-data; boundary=/);
206
+ assert.match(body.toString(), /name="task_id"\r\n\r\n7684370037737553941\r\n/);
207
+ assert.match(body.toString(), /name="import_token"\r\n\r\nprocess-test-secret\r\n/);
208
+ assert.ok(body.includes(fs.readFileSync(iconUpload ? icon : zip)));
209
+ if (packageUpload) assert.match(body.toString(), /name="asset_type"\r\n\r\n2\r\n/);
210
+ }
211
+ }
167
212
  const data = create ? {
168
213
  task_id: '7684370037737553941', import_token: 'process-test-secret',
169
- landing_url: 'https://developers.tiktok.com/portal_h5/minis_import?import_token=process-test-secret',
214
+ landing_url: 'https://developers.tiktok.com/portal_h5/minis_import?task_id=7684370037737553941&import_token=process-test-secret',
170
215
  poll_interval_seconds: 0.01,
171
- } : { status: 1, ready_for_input: false };
216
+ } : iconUpload ? {icon_uri: 'web-icon-receipt'}
217
+ : infoUpload ? {}
218
+ : packageUpload ? {status: 3, package_uri: 'web-package-receipt'}
219
+ : { status: webMode && webPolls >= 5 ? 2 : 1, ready_for_input: webMode && webPolls >= 5 };
172
220
  res.writeHead(200, { 'Content-Type': 'application/json' });
173
- res.end(JSON.stringify({ ...data, error_info: { code: failStatus && status ? 420500 : 0 } }));
221
+ res.end(JSON.stringify({ ...data, error_info: { code: (failStatus && status) || url.pathname === webFailureEndpoint ? 420500 : 0 } }));
174
222
  });
175
223
  await new Promise(resolve => fixtureServer.listen(0, '127.0.0.1', resolve));
176
224
  env.TTMG_VIBE_TEST_PORT = String(fixtureServer.address().port);
@@ -189,6 +237,13 @@ async function main() {
189
237
 
190
238
  let bindingHtml;
191
239
  const appInfoPath = path.join(temp, 'app-info.json');
240
+ const invalidLegacy = await run([...input, '--app-info', appInfoPath, '--dry-run', '--format', 'json']);
241
+ assert.equal(invalidLegacy.code, 1);
242
+ const legacyFailure = events(invalidLegacy)[0];
243
+ assert.equal(legacyFailure.errorCode, 'VIBE_INVALID_APP_INFO');
244
+ assert.equal(legacyFailure.platformWrite, false);
245
+ assert.match(legacyFailure.message, /Remove --app-info|移除 --app-info/);
246
+ assert.doesNotMatch(legacyFailure.message, /are required|需要简介/);
192
247
  fs.writeFileSync(appInfoPath, JSON.stringify({
193
248
  description: 'Process contract fixture', category: 'Casual', subcategory: 'Puzzle',
194
249
  terms_of_service: 'https://example.invalid/terms', privacy_policy: 'https://example.invalid/privacy',
@@ -207,6 +262,14 @@ async function main() {
207
262
  const completeDry = await run([...input, '--app-info', appInfoPath, '--dry-run', '--format', 'json']);
208
263
  assert.equal(completeDry.code, 0, completeDry.stderr);
209
264
  assert.equal(events(completeDry)[0].status, 'prepared');
265
+ assert.equal(events(completeDry)[0].classification.category, 'Casual');
266
+ assert.equal(events(completeDry)[0].classification.subcategory, 'Puzzle');
267
+ assert.equal(events(completeDry)[0].classification.source, 'app-info');
268
+ const overrideDry = await run([...input, '--app-info', appInfoPath, '--category', 'Puzzle', '--subcategory', 'Physics', '--dry-run', '--format', 'json']);
269
+ assert.equal(overrideDry.code, 0, overrideDry.stderr);
270
+ assert.equal(events(overrideDry)[0].classification.category, 'Puzzle');
271
+ assert.equal(events(overrideDry)[0].classification.subcategory, 'Physics');
272
+ assert.equal(events(overrideDry)[0].classification.source, 'arguments');
210
273
  assert.equal(requests.length, 0);
211
274
  fs.writeFileSync(appInfoPath, '{private-token');
212
275
  const badInfo = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
@@ -310,6 +373,102 @@ async function main() {
310
373
  assert.match(previewHtml, /data-phase="complete"/);
311
374
  assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
312
375
 
376
+ const beforeWeb = requests.length;
377
+ const webConflict = await run([...input, '--web', '--mock', '--format', 'json']);
378
+ assert.equal(events(webConflict)[0].errorCode, 'VIBE_WEB_OPTION_CONFLICT');
379
+ const legacyWeb = await run(['upload', '--web', '--format', 'json']);
380
+ assert.equal(events(legacyWeb)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
381
+ assert.equal(requests.length, beforeWeb);
382
+ for (const [args, code] of [
383
+ [['--category', 'Puzzle'], 'VIBE_CATEGORY_PAIR_REQUIRED'],
384
+ [['--subcategory', 'Physics'], 'VIBE_CATEGORY_PAIR_REQUIRED'],
385
+ [['--category', 'Other', '--subcategory', 'Physics'], 'VIBE_INVALID_CATEGORY'],
386
+ [['--category', 'Unknown', '--subcategory', 'Other'], 'VIBE_INVALID_CATEGORY'],
387
+ [['--category', '', '--subcategory', ''], 'VIBE_INVALID_CATEGORY'],
388
+ ]) {
389
+ const invalid = await run([...input.filter(v => v !== '--no-open'), '--web', ...args, '--format', 'json']);
390
+ assert.equal(invalid.code, 1);
391
+ const result = events(invalid)[0];
392
+ assert.equal(result.errorCode, code);
393
+ assert.equal(result.platformWrite, false);
394
+ assert.equal(result.taskId, undefined);
395
+ assert.equal(result.pageUrl, undefined);
396
+ }
397
+ assert.equal(requests.length, beforeWeb, 'Invalid classification must not create tasks or upload');
398
+ webMode = true;
399
+ for (const format of ['json', 'ndjson']) {
400
+ expectedWebCategory = format === 'json' ? {category: 'Other', subcategory: 'Other'} : {category: 'Puzzle', subcategory: 'Physics'};
401
+ const categoryArgs = format === 'json' ? [] : ['--category', 'Puzzle', '--subcategory', 'Physics'];
402
+ const receipt = path.join(temp, `upload-web-${format}.ndjson`);
403
+ const requestStart = requests.length;
404
+ const web = await run([...input.filter(v => v !== '--no-open'), '--web', ...categoryArgs, '--format', format, '--poll-interval', '0.2'], async event => {
405
+ if (event.status === 'prepared') assert.deepEqual(
406
+ {category: event.classification.category, subcategory: event.classification.subcategory},
407
+ expectedWebCategory);
408
+ if (event.status === 'waiting_authorization') {
409
+ assert.equal(event.authorizationMode, 'web');
410
+ assert.equal(event.browserOpenRequested, true);
411
+ assert.equal(event.simulation, false);
412
+ assert.equal(event.nextAction, 'open_browser');
413
+ assert.equal(event.qrPurpose, undefined);
414
+ const response = await fetch(event.pageUrl + '/authorize', {redirect: 'manual'});
415
+ assert.equal(response.status, 302);
416
+ assert.match(response.headers.get('location'), /^https:\/\/developers\.tiktok\.com\/portal_h5\/minis_import\?/);
417
+ const html = await (await fetch(event.pageUrl)).text();
418
+ assert.match(html, /Authorize in your browser/);
419
+ assert.doesNotMatch(html, /alt="Authorization QR"|Scan the QR code/);
420
+ }
421
+ }, {
422
+ NODE_OPTIONS: env.NODE_OPTIONS + ' --require=' + path.join(__dirname, 'vibe-auth-open-test-preload.cjs'),
423
+ TTMG_AUTH_OPEN_TEST_RECEIPT: receipt,
424
+ // Explicit --web must not inherit an old Mock backend setting.
425
+ TTMG_VIBE_BACKEND: 'mock',
426
+ TTMG_VIBE_TEST_RETAIN_UPLOAD_SOCKET: '1',
427
+ });
428
+ assert.equal(web.code, 0, web.stderr);
429
+ const output = events(web);
430
+ assert.equal(output.at(-1).status, 'uploaded');
431
+ assert.equal(output.at(-1).simulation, false);
432
+ assert.equal(output.at(-1).backend, 'ppe');
433
+ assert.equal(output.at(-1).readyForInput, true);
434
+ assert.equal(output.at(-1).uploadAttempted, true);
435
+ assert.equal(output.at(-1).uploaded, true);
436
+ assert.deepEqual(
437
+ { category: output.at(-1).classification.category, subcategory: output.at(-1).classification.subcategory },
438
+ expectedWebCategory);
439
+ assert.equal(output.at(-1).classification.source, format === 'json' ? 'defaults' : 'arguments');
440
+ assert.equal(output.at(-1).taskStatusRaw, 3);
441
+ assert.equal(output.filter(e => e.terminal).length, 1);
442
+ assert.equal(webPolls, 5, 'Only true backend readiness unlocks authorization');
443
+ assert.deepEqual(requests.slice(requestStart).map(r => r.path.split('/').filter(Boolean).at(-1)), [
444
+ 'create_minis_import_task_for_agent', ...Array(5).fill('get_minis_import_task_status_for_agent'),
445
+ 'upload_minis_import_app_icon_for_agent', 'submit_minis_import_info_for_agent', 'upload_minis_import_package_for_agent',
446
+ ]);
447
+ if (format === 'json') assert.equal(output.length, 1);
448
+ const opens = fs.readFileSync(receipt, 'utf8').trim().split('\n').map(line => JSON.parse(line));
449
+ assert.equal(opens.length, 1);
450
+ assert.ok(opens[0].args.some(arg => /^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{48}\/authorize$/.test(arg)));
451
+ assert.doesNotMatch(JSON.stringify(opens) + web.stdout + web.stderr, /process-test-secret|import_token|web-icon-receipt|web-package-receipt|aweme:\/\//);
452
+ }
453
+ expectedWebCategory = {category: 'Other', subcategory: 'Other'};
454
+ webFailureEndpoint = '/bff/app_info/minis_import/submit_minis_import_info_for_agent/';
455
+ const failedStart = requests.length;
456
+ const webUploadFailure = await run([...input.filter(v => v !== '--no-open'), '--web', '--format', 'json'], undefined, {
457
+ NODE_OPTIONS: env.NODE_OPTIONS + ' --require=' + path.join(__dirname, 'vibe-auth-open-test-preload.cjs'),
458
+ TTMG_AUTH_OPEN_TEST_RECEIPT: path.join(temp, 'web-failure-open.ndjson'),
459
+ TTMG_VIBE_TEST_RETAIN_UPLOAD_SOCKET: '1',
460
+ });
461
+ assert.equal(webUploadFailure.code, 1);
462
+ const webFailure = events(webUploadFailure)[0];
463
+ assert.equal(webFailure.errorCode, 'VIBE_BACKEND_REJECTED');
464
+ assert.equal(webFailure.uploadAttempted, true);
465
+ assert.equal(webFailure.uploaded, false);
466
+ assert.equal(webFailure.simulation, false);
467
+ assert.equal(requests.at(-1).path, webFailureEndpoint);
468
+ assert.equal(requests.slice(failedStart).filter(r => r.path === webFailureEndpoint).length, 1);
469
+ webFailureEndpoint = undefined;
470
+ webMode = false;
471
+
313
472
  const json = await run(
314
473
  [...input, '--format', 'json', '--poll-interval', '0.01'],
315
474
  undefined,
@@ -428,6 +587,29 @@ async function main() {
428
587
  const badMode = await run(['upload', '--title', 'x', '--format', 'json']);
429
588
  assert.equal(badMode.code, 1);
430
589
  assert.equal(events(badMode)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
590
+ const badCategoryMode = await run(['upload', '--category', 'Puzzle', '--subcategory', 'Physics', '--format', 'json']);
591
+ assert.equal(events(badCategoryMode)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
592
+ const beforeCatalog = requests.length;
593
+ const catalog = await run(['game', 'categories', '--format', 'json']);
594
+ assert.equal(catalog.code, 0, catalog.stderr);
595
+ assert.equal(events(catalog).length, 1);
596
+ assert.equal(events(catalog)[0].categoryCount, 12);
597
+ assert.equal(events(catalog)[0].subcategoryCount, 114);
598
+ assert.equal(events(catalog)[0].platformWrite, false);
599
+ const filtered = await run(['game', 'categories', '--category', 'Puzzle', '--format', 'ndjson']);
600
+ assert.equal(filtered.code, 0);
601
+ assert.deepEqual(events(filtered)[0].categories.map(item => item.value), ['Puzzle']);
602
+ assert.ok(events(filtered)[0].categories[0].subcategories.every(item => item.description));
603
+ for (const [args, code] of [
604
+ [['--category', 'unknown'], 'GAME_CATEGORY_UNKNOWN'],
605
+ [['--format', 'xml'], 'GAME_CATEGORIES_INVALID_FORMAT'],
606
+ ]) {
607
+ const invalid = await run(['game', 'categories', '--format', 'json', ...args]);
608
+ assert.equal(invalid.code, 1);
609
+ assert.equal(events(invalid)[0].errorCode, code);
610
+ assert.equal(events(invalid)[0].platformWrite, false);
611
+ }
612
+ assert.equal(requests.length, beforeCatalog, 'Catalog commands must remain offline');
431
613
  const help = await run(['upload', '--help']);
432
614
  assert.equal(help.code, 0);
433
615
  assert.match(help.stdout, /--vibe/);
@@ -0,0 +1,78 @@
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 crypto = require('node:crypto');
6
+ const http = require('node:http');
7
+ const { spawn } = require('node:child_process');
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-collect-process-'));
9
+ const entry = path.resolve(__dirname, '../dist/index.js');
10
+ const preload = path.resolve(__dirname, 'wasm-collect-test-preload.cjs');
11
+ const input = path.join(root, 'original.wasm');
12
+ const wasm = Buffer.from('0061736d010000000105016000017f0304030000000404017000030707010372756e00000909010041000b030001020a1003040010010b0400412a0b040041070b', 'hex');
13
+ fs.writeFileSync(input, wasm);
14
+ const md5 = crypto.createHash('md5').update(wasm).digest('hex');
15
+ const rc = path.join(root, 'rc.json');
16
+ const secret = 'sessionid=TEST_ONLY_COLLECT_SECRET';
17
+ fs.writeFileSync(rc, JSON.stringify({ cookie: secret }));
18
+ let mode = 'success'; const calls = [];
19
+ const server = http.createServer(async (req, res) => {
20
+ let body = ''; for await (const b of req) body += b;
21
+ calls.push({ url: req.url, method: req.method, body: body ? JSON.parse(body) : null, cookie: req.headers.cookie });
22
+ res.setHeader('content-type', 'application/json');
23
+ const data = mode === 'auth' ? { code: -401, message: secret }
24
+ : req.url.includes('/start') ? { code: 0, collect_state: 'open', cookie: secret }
25
+ : { code: 0, func_ids: [1, 0, 1], boot_func_ids: [0], cookie: secret };
26
+ res.end(JSON.stringify(data));
27
+ });
28
+ async function run(args) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(process.execPath, ['-r', preload, entry, ...args], { cwd: root,
31
+ env: { ...process.env, TTMGRC_PATH: rc, TTMG_COLLECT_TEST_PORT: String(server.address().port),
32
+ HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '', all_proxy: '',
33
+ NO_COLOR: '1', FORCE_COLOR: '0' } });
34
+ let stdout = '', stderr = '';
35
+ child.stdout.on('data', b => stdout += b); child.stderr.on('data', b => stderr += b);
36
+ const timer = setTimeout(() => { child.kill('SIGKILL'); reject(Error('CLI timeout')); }, 15000);
37
+ child.on('error', reject); child.on('close', code => { clearTimeout(timer); resolve({ code, stdout, stderr }); });
38
+ });
39
+ }
40
+ function result(r, code = 0) {
41
+ assert.equal(r.code, code, r.stderr || r.stdout);
42
+ assert.equal(r.stdout.trim().split('\n').length, 1, r.stdout);
43
+ assert.equal((r.stdout+r.stderr).includes(secret), false);
44
+ return JSON.parse(r.stdout);
45
+ }
46
+ (async () => {
47
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
48
+ const args = ['--client-key','fixture','--wasm-md5',md5,'--format','json'];
49
+ const a = result(await run(['wasm','collect','start', ...args, '--verbose']));
50
+ assert.equal(a.collectState,'open'); assert.equal(calls[0].cookie,secret);
51
+ assert.equal(calls[0].body.reset,false);
52
+ result(await run(['wasm','collect','start',...args,'--reset']));
53
+ assert.equal(calls[1].body.reset,true);
54
+ const beforeDefault = calls.length;
55
+ const defaultJson = result(await run(['--verbose', 'wasm','collect','start','--client-key','fixture','--wasm-md5',md5]));
56
+ assert.equal(defaultJson.collectState,'open');assert.equal(calls.length,beforeDefault+1);
57
+ const output=path.join(root,'collection.json');
58
+ const exported=result(await run(['wasm','collect','export',...args,'--input',input,'--output',output,'--verbose']));
59
+ assert.equal(exported.collection.path,output);assert.equal(calls[3].method,'GET');
60
+ assert.equal(calls[3].cookie,secret);assert.match(calls[3].url,/strategy=union/);
61
+ const collection=JSON.parse(fs.readFileSync(output,'utf8'));
62
+ assert.deepEqual(collection.funcIds,[0,1]);assert.deepEqual(collection.bootFuncIds,[0]);
63
+ assert.equal(fs.readFileSync(output,'utf8').includes(secret),false);
64
+ const split=result(await run(['wasm','split','--input',input,'--collection',output,
65
+ '--output',path.join(root,'split'),'--dry-run','--format','json']));
66
+ assert.equal(split.status,'ready');
67
+ mode='auth';
68
+ assert.equal(result(await run(['wasm','collect','start',...args,'--verbose']),1).errorCode,'WASM_AUTH_REQUIRED');
69
+ const count=calls.length;fs.writeFileSync(rc,'{}');
70
+ assert.equal(result(await run(['wasm','collect','start',...args]),1).errorCode,'WASM_AUTH_REQUIRED');
71
+ assert.equal(calls.length,count);
72
+ const unknown=await run(['wasm','collect','export',...args,'--reset']);assert.notEqual(unknown.code,0);
73
+ assert.equal(calls.length,count);
74
+ assert.deepEqual(fs.readFileSync(input),wasm);
75
+ console.log('Wasm collect process contract passed: local login transport, reset, export-to-split, clean verbose output; fixture only');
76
+ })().catch(e => { console.error(e); process.exitCode=1; }).finally(() => {
77
+ server.closeAllConnections();server.close();fs.rmSync(root,{recursive:true,force:true});
78
+ });
@@ -0,0 +1,13 @@
1
+ // Test-only transport: credentials and platform responses are synthetic.
2
+ const assert = require('node:assert/strict');
3
+ const https = require('node:https');
4
+ const http = require('node:http');
5
+ const port = Number(process.env.TTMG_COLLECT_TEST_PORT);
6
+ assert.ok(Number.isInteger(port) && port > 0 && port < 65536);
7
+ https.request = (options, callback) => {
8
+ assert.equal(options.hostname, 'developers.tiktok.com');
9
+ assert.match(options.path, /^\/api\/wasm-collect\/v1\/(start|export)(\?|$)/);
10
+ return http.request({ ...options, protocol: 'http:', hostname: '127.0.0.1', host: undefined,
11
+ port, agent: undefined, servername: undefined }, callback);
12
+ };
13
+ https.get = (options, callback) => { const req = https.request(options, callback); req.end(); return req; };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-agent-beta.2",
3
+ "version": "0.4.6-agent-beta.3",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
7
7
  "ttmg": "dist/index.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "vibe-upload.md"
11
12
  ],
12
13
  "publishConfig": {
13
14
  "access": "public",
@@ -15,7 +16,8 @@
15
16
  },
16
17
  "scripts": {
17
18
  "demo:vibe": "npm run build && node scripts/vibe-upload-demo.js",
18
- "test:vibe": "node scripts/run-ts-test.js src/commands/common/vibe/vibe.test.ts",
19
+ "test:game-categories": "node scripts/run-ts-test.js src/commands/common/game/categories.test.ts",
20
+ "test:vibe": "npm run test:game-categories && node scripts/run-ts-test.js src/commands/common/vibe/vibe.test.ts",
19
21
  "test:vibe-process": "node scripts/vibe-upload-smoke.js",
20
22
  "prepack": "npm run build && npm run verify:dist-version && npm run verify:agent-contract",
21
23
  "build": "node scripts/build.js",
@@ -46,7 +48,7 @@
46
48
  "test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
47
49
  "test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
48
50
  "test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
49
- "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
51
+ "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/run-ts-test.js src/commands/native/wasm/collect.test.ts && node scripts/acceptance-fixture.test.js",
50
52
  "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",
51
53
  "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",
52
54
  "publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag beta",