@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/README.md +105 -13
- package/dist/index.js +1167 -168
- package/dist/index.js.map +1 -1
- package/dist/package.json +6 -4
- package/dist/scripts/verify-agent-contract.js +30 -0
- package/dist/scripts/vibe-ppe-test-preload.cjs +16 -2
- package/dist/scripts/vibe-upload-smoke.js +189 -7
- package/dist/scripts/wasm-collect-smoke.js +78 -0
- package/dist/scripts/wasm-collect-test-preload.cjs +13 -0
- package/package.json +6 -4
- package/vibe-upload.md +358 -0
package/dist/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttmg/cli",
|
|
3
|
-
"version": "0.4.6-agent-beta.
|
|
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:
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
} :
|
|
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.
|
|
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:
|
|
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",
|