@ttmg/cli 0.4.6-agent-beta.1 → 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 +119 -12
- package/dist/index.js +1371 -165
- package/dist/index.js.map +1 -1
- package/dist/package.json +9 -6
- package/dist/scripts/dev-default-smoke.js +22 -1
- package/dist/scripts/verify-agent-contract.js +70 -2
- package/dist/scripts/vibe-auth-open-test-preload.cjs +11 -0
- package/dist/scripts/vibe-ppe-test-preload.cjs +16 -2
- package/dist/scripts/vibe-upload-smoke.js +243 -7
- package/dist/scripts/wasm-collect-smoke.js +78 -0
- package/dist/scripts/wasm-collect-test-preload.cjs +13 -0
- package/package.json +9 -6
- 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",
|
|
@@ -30,7 +32,8 @@
|
|
|
30
32
|
"test:auth": "node scripts/run-ts-test.js src/commands/common/auth.test.ts",
|
|
31
33
|
"test:capabilities": "node scripts/run-ts-test.js src/commands/common/capabilities.test.ts",
|
|
32
34
|
"test:test-account": "node scripts/run-ts-test.js src/commands/common/testAccount.test.ts",
|
|
33
|
-
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts",
|
|
35
|
+
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts && npm run test:packaging-compatibility",
|
|
36
|
+
"test:packaging-compatibility": "node scripts/run-ts-test.js src/commands/native/dev/packagingCompatibility.test.ts",
|
|
34
37
|
"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",
|
|
35
38
|
"test:dev-runtime-event": "node scripts/run-ts-test.js src/commands/native/dev/utils/scoket.test.ts",
|
|
36
39
|
"test:dev-runtime-client": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtime.test.ts",
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
"test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
|
|
46
49
|
"test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
|
|
47
50
|
"test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
|
|
48
|
-
"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",
|
|
49
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",
|
|
50
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",
|
|
51
54
|
"publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag beta",
|
|
@@ -93,7 +96,7 @@
|
|
|
93
96
|
"qs": "6.12.1",
|
|
94
97
|
"semver": "^7.7.2",
|
|
95
98
|
"socks-proxy-agent": "^9.0.0",
|
|
96
|
-
"ttmg-pack": "0.4.13
|
|
99
|
+
"ttmg-pack": "0.4.13",
|
|
97
100
|
"ws": "^8.18.3"
|
|
98
101
|
},
|
|
99
102
|
"devDependencies": {
|
|
@@ -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');
|
|
@@ -10,7 +10,7 @@ const packageJson = JSON.parse(
|
|
|
10
10
|
fs.readFileSync(path.join(cliRoot, 'package.json'), 'utf8'),
|
|
11
11
|
);
|
|
12
12
|
|
|
13
|
-
function runCli(args, environment = {}) {
|
|
13
|
+
function runCli(args, environment = {}, cwd = cliRoot) {
|
|
14
14
|
const env = {
|
|
15
15
|
...process.env,
|
|
16
16
|
NO_COLOR: '1',
|
|
@@ -20,7 +20,7 @@ function runCli(args, environment = {}) {
|
|
|
20
20
|
delete env.TTMG_LOGIN_EMAIL;
|
|
21
21
|
delete env.TTMG_LOGIN_PASSWORD;
|
|
22
22
|
return spawnSync(process.execPath, [cliEntry, ...args], {
|
|
23
|
-
cwd
|
|
23
|
+
cwd,
|
|
24
24
|
env,
|
|
25
25
|
encoding: 'utf8',
|
|
26
26
|
});
|
|
@@ -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');
|
|
@@ -270,6 +282,44 @@ assert.equal(parseSingleJson(invalidBackground, 'background').errorCode, 'DEV_IT
|
|
|
270
282
|
const invalidManual = runCli(['dev', '--update-mode', 'invalid', '--format', 'json']);
|
|
271
283
|
assertExit(invalidManual, 1, 'update mode validation');
|
|
272
284
|
assert.equal(parseSingleJson(invalidManual, 'manual').errorCode, 'DEV_ITERATION_INVALID_OPTIONS');
|
|
285
|
+
// Validation must happen before writing evidence or starting local services.
|
|
286
|
+
const unsafeEvidence = path.join(cliRoot, 'unsafe-evidence-' + process.pid);
|
|
287
|
+
const invalidEvidence = runCli(['dev', '--client-key', 'fixture', '--mode', 'client-preview', '--no-open', '--format', 'json', '--evidence-dir', unsafeEvidence]);
|
|
288
|
+
assertExit(invalidEvidence, 1, 'evidence directory isolation');
|
|
289
|
+
assert.equal(parseSingleJson(invalidEvidence, 'evidence directory').errorCode, 'DEV_EVIDENCE_DIRECTORY_UNSAFE');
|
|
290
|
+
assert.equal(fs.existsSync(unsafeEvidence), false);
|
|
291
|
+
assert.deepEqual(capabilities.commands.dev.sessionLogs.evidenceDirectoryLocations, ['outside-project', 'project-root-.ttmg']);
|
|
292
|
+
const packagingFixture = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-installed-pack-gate-'));
|
|
293
|
+
try {
|
|
294
|
+
fs.writeFileSync(path.join(packagingFixture, 'game.js'), 'console.log("fixture");');
|
|
295
|
+
fs.writeFileSync(path.join(packagingFixture, 'game.json'), '{}');
|
|
296
|
+
fs.mkdirSync(path.join(packagingFixture, '.ttmg'));
|
|
297
|
+
fs.writeFileSync(path.join(packagingFixture, '.ttmg/logs.ndjson'), Buffer.alloc(5 * 1024 * 1024));
|
|
298
|
+
fs.writeFileSync(path.join(packagingFixture, '.ttmg/旧构建.js'), 'tt.login();');
|
|
299
|
+
const checked = runCli(['check', '--dir', packagingFixture, '--format', 'json']);
|
|
300
|
+
assertExit(checked, 0, 'installed packaging evidence exclusion');
|
|
301
|
+
const report = parseSingleJson(checked, 'installed packaging evidence exclusion');
|
|
302
|
+
assert.ok(!report.diagnostics.some(item => item.type === 'size' && !item.passed), 'installed ttmg-pack counted CLI evidence in package size');
|
|
303
|
+
assert.ok(report.diagnostics.some(item => item.name === 'login' && !item.passed), 'installed ttmg-pack scanned APIs from old debug builds');
|
|
304
|
+
} finally {
|
|
305
|
+
fs.rmSync(packagingFixture, { recursive: true, force: true });
|
|
306
|
+
}
|
|
307
|
+
if (process.platform !== 'win32') {
|
|
308
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-evidence-symlink-'));
|
|
309
|
+
try {
|
|
310
|
+
fs.mkdirSync(path.join(root, 'assets'));
|
|
311
|
+
fs.writeFileSync(path.join(root, 'game.js'), 'console.log("fixture");');
|
|
312
|
+
fs.symlinkSync(path.join(root, 'assets'), path.join(root, '.ttmg'));
|
|
313
|
+
for (const extra of [[], ['--background']]) {
|
|
314
|
+
const result = runCli(['dev', '--mode', 'client-preview', '--update-mode', 'manual', '--client-key', 'fixture', '--no-open', '--format', 'json', ...extra], {}, root);
|
|
315
|
+
assertExit(result, 1, 'symlinked evidence isolation');
|
|
316
|
+
assert.equal(parseSingleJson(result, 'symlinked evidence').errorCode, 'DEV_EVIDENCE_DIRECTORY_UNSAFE');
|
|
317
|
+
assert.deepEqual(fs.readdirSync(path.join(root, 'assets')), [], 'validation wrote into game assets');
|
|
318
|
+
}
|
|
319
|
+
} finally {
|
|
320
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
273
323
|
// Both formats perform the same local build validation; the former text placeholder is retired.
|
|
274
324
|
const missingBuildDirectory = path.join(os.tmpdir(), 'ttmg-build-dir-that-does-not-exist-' + process.pid);
|
|
275
325
|
for (const format of ['text', 'json']) {
|
|
@@ -347,7 +397,14 @@ assert.notEqual(
|
|
|
347
397
|
);
|
|
348
398
|
|
|
349
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|只收集标题和头像/);
|
|
350
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>/);
|
|
351
408
|
assert.match(uploadHelp.stdout, /--preview\b/);
|
|
352
409
|
assert.match(uploadHelp.stdout, /--serve-preview-page\b/);
|
|
353
410
|
assert.match(uploadHelp.stdout, /--no-open\b/);
|
|
@@ -394,6 +451,17 @@ const wasmSmoke = spawnSync(process.execPath, [path.join(cliRoot, 'scripts/wasm-
|
|
|
394
451
|
});
|
|
395
452
|
assertExit(wasmSmoke, 0, 'Wasm built-artifact process smoke');
|
|
396
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());
|
|
397
465
|
const captureHelp = runCli(['dev', 'capture', '--help']);
|
|
398
466
|
assertExit(captureHelp, 0, 'capture help');
|
|
399
467
|
assert.match(captureHelp.stdout, /--timeout/);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Test-only browser boundary: never launch a real browser from process smoke.
|
|
2
|
+
const cp = require('node:child_process');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const original = cp.spawn;
|
|
5
|
+
cp.spawn = function(command, args, options) {
|
|
6
|
+
if (command === 'open' || /(?:xdg-open|powershell\.exe)$/i.test(command)) {
|
|
7
|
+
fs.appendFileSync(process.env.TTMG_AUTH_OPEN_TEST_RECEIPT, JSON.stringify({command, args}) + '\n');
|
|
8
|
+
return original(process.execPath, ['-e', 'process.exit(0)'], {stdio: 'ignore'});
|
|
9
|
+
}
|
|
10
|
+
return original.call(this, command, args, options);
|
|
11
|
+
};
|
|
@@ -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,16 +100,83 @@ 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.
|
|
116
|
+
const authWorker = spawn(process.execPath, [entry, '__vibe-page-server'], {
|
|
117
|
+
env: {...env, TTMG_VIBE_PAGE_TTL_MS: '30000'}, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
118
|
+
});
|
|
119
|
+
children.add(authWorker);
|
|
120
|
+
const workerMessage = predicate => new Promise((resolve, reject) => {
|
|
121
|
+
const timer = setTimeout(() => {authWorker.off('message', receive); reject(new Error('worker timeout'));}, 5000);
|
|
122
|
+
function receive(message) {
|
|
123
|
+
if (predicate(message)) {clearTimeout(timer); authWorker.off('message', receive); resolve(message);}
|
|
124
|
+
}
|
|
125
|
+
authWorker.on('message', receive);
|
|
126
|
+
});
|
|
127
|
+
const ready = await workerMessage(message => message.type === 'ready');
|
|
128
|
+
const landing = 'https://developers.tiktok.com/portal_h5/minis_import?task_id=123&import_token=process-private-token';
|
|
129
|
+
let rendered = workerMessage(message => message.type === 'rendered' && message.id === 1);
|
|
130
|
+
authWorker.send({type: 'render', id: 1, html: '<h1>Waiting</h1>', browserLandingUrl: landing});
|
|
131
|
+
await rendered;
|
|
132
|
+
const browserReceipt = path.join(temp, 'browser-open.ndjson');
|
|
133
|
+
const browserEnv = {
|
|
134
|
+
NODE_OPTIONS: `--require=${path.join(__dirname, 'vibe-auth-open-test-preload.cjs')}`,
|
|
135
|
+
TTMG_AUTH_OPEN_TEST_RECEIPT: browserReceipt,
|
|
136
|
+
};
|
|
137
|
+
for (const format of ['json', 'ndjson']) {
|
|
138
|
+
const opened = await run(['auth', 'open', '--page-url', ready.url, '--format', format], undefined, browserEnv);
|
|
139
|
+
assert.equal(opened.code, 0, opened.stderr);
|
|
140
|
+
const result = JSON.parse(opened.stdout);
|
|
141
|
+
assert.equal(result.status, 'open_requested');
|
|
142
|
+
assert.equal(result.platformWrite, false);
|
|
143
|
+
assert.doesNotMatch(opened.stdout + opened.stderr, /import_token|process-private-token/);
|
|
144
|
+
}
|
|
145
|
+
const launches = fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').map(JSON.parse);
|
|
146
|
+
assert.equal(launches.length, 2);
|
|
147
|
+
assert.ok(launches.every(item => item.args.includes(ready.url + '/authorize')));
|
|
148
|
+
const redirect = await fetch(ready.url + '/authorize', {redirect: 'manual'});
|
|
149
|
+
assert.equal(redirect.headers.get('location'), landing);
|
|
150
|
+
assert.equal(redirect.status, 302);
|
|
151
|
+
rendered = workerMessage(message => message.type === 'rendered' && message.id === 2);
|
|
152
|
+
authWorker.send({type: 'render', id: 2, html: '<h1>Complete</h1>'});
|
|
153
|
+
await rendered;
|
|
154
|
+
const stale = await run(['auth', 'open', '--page-url', ready.url, '--format', 'json'], undefined, browserEnv);
|
|
155
|
+
assert.equal(stale.code, 1);
|
|
156
|
+
assert.equal(JSON.parse(stale.stdout).errorCode, 'VIBE_AUTH_PAGE_UNAVAILABLE');
|
|
157
|
+
for (const args of [[], ['--page-url', landing]]) {
|
|
158
|
+
const invalid = await run(['auth', 'open', ...args, '--format', 'json'], undefined, browserEnv);
|
|
159
|
+
assert.equal(invalid.code, 1);
|
|
160
|
+
assert.equal(JSON.parse(invalid.stdout).errorCode, 'VIBE_INVALID_PAGE_URL');
|
|
161
|
+
assert.doesNotMatch(invalid.stdout + invalid.stderr, /process-private-token/);
|
|
162
|
+
}
|
|
163
|
+
assert.equal(fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').length, 2);
|
|
164
|
+
authWorker.kill();
|
|
165
|
+
children.delete(authWorker);
|
|
166
|
+
fixtureServer = http.createServer(async (req, res) => {
|
|
103
167
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
104
168
|
requests.push({ path: url.pathname, method: req.method, headers: req.headers });
|
|
105
|
-
|
|
169
|
+
const chunks = [];
|
|
170
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
171
|
+
const body = Buffer.concat(chunks);
|
|
106
172
|
const create = url.pathname.endsWith('/create_minis_import_task_for_agent/');
|
|
107
173
|
const status = url.pathname.endsWith('/get_minis_import_task_status_for_agent/');
|
|
108
|
-
|
|
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');
|
|
109
180
|
assert.equal(req.headers['x-use-ppe'], '1');
|
|
110
181
|
assert.equal(req.headers['x-tt-env'], 'ppe_op_vc');
|
|
111
182
|
assert.equal(req.headers.cookie, undefined);
|
|
@@ -113,13 +184,41 @@ async function main() {
|
|
|
113
184
|
assert.equal(url.searchParams.get('task_id'), '7684370037737553941');
|
|
114
185
|
assert.equal(url.searchParams.get('import_token'), 'process-test-secret');
|
|
115
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
|
+
}
|
|
116
212
|
const data = create ? {
|
|
117
213
|
task_id: '7684370037737553941', import_token: 'process-test-secret',
|
|
118
|
-
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',
|
|
119
215
|
poll_interval_seconds: 0.01,
|
|
120
|
-
} :
|
|
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 };
|
|
121
220
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
122
|
-
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 } }));
|
|
123
222
|
});
|
|
124
223
|
await new Promise(resolve => fixtureServer.listen(0, '127.0.0.1', resolve));
|
|
125
224
|
env.TTMG_VIBE_TEST_PORT = String(fixtureServer.address().port);
|
|
@@ -138,6 +237,13 @@ async function main() {
|
|
|
138
237
|
|
|
139
238
|
let bindingHtml;
|
|
140
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|需要简介/);
|
|
141
247
|
fs.writeFileSync(appInfoPath, JSON.stringify({
|
|
142
248
|
description: 'Process contract fixture', category: 'Casual', subcategory: 'Puzzle',
|
|
143
249
|
terms_of_service: 'https://example.invalid/terms', privacy_policy: 'https://example.invalid/privacy',
|
|
@@ -156,6 +262,14 @@ async function main() {
|
|
|
156
262
|
const completeDry = await run([...input, '--app-info', appInfoPath, '--dry-run', '--format', 'json']);
|
|
157
263
|
assert.equal(completeDry.code, 0, completeDry.stderr);
|
|
158
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');
|
|
159
273
|
assert.equal(requests.length, 0);
|
|
160
274
|
fs.writeFileSync(appInfoPath, '{private-token');
|
|
161
275
|
const badInfo = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
|
|
@@ -259,6 +373,102 @@ async function main() {
|
|
|
259
373
|
assert.match(previewHtml, /data-phase="complete"/);
|
|
260
374
|
assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
|
|
261
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
|
+
|
|
262
472
|
const json = await run(
|
|
263
473
|
[...input, '--format', 'json', '--poll-interval', '0.01'],
|
|
264
474
|
undefined,
|
|
@@ -377,10 +587,36 @@ async function main() {
|
|
|
377
587
|
const badMode = await run(['upload', '--title', 'x', '--format', 'json']);
|
|
378
588
|
assert.equal(badMode.code, 1);
|
|
379
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');
|
|
380
613
|
const help = await run(['upload', '--help']);
|
|
381
614
|
assert.equal(help.code, 0);
|
|
382
615
|
assert.match(help.stdout, /--vibe/);
|
|
383
616
|
assert.match(help.stdout, /--client-key/);
|
|
617
|
+
const authHelp = await run(['auth', 'open', '--help']);
|
|
618
|
+
assert.equal(authHelp.code, 0);
|
|
619
|
+
assert.match(authHelp.stdout, /--page-url/);
|
|
384
620
|
|
|
385
621
|
let cancelledPage;
|
|
386
622
|
const cancelled = await run(
|