@ttmg/cli 0.4.6-vibe-beta.6 → 0.4.6-vibe-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +427 -11
- package/dist/index.js +3590 -205
- package/dist/index.js.map +1 -1
- package/dist/package.json +11 -9
- package/dist/scripts/acceptance-fixture.test.js +33 -0
- package/dist/scripts/dev-capture-smoke.js +76 -0
- package/dist/scripts/dev-client-preview-control-smoke.js +293 -9
- package/dist/scripts/dev-default-smoke.js +22 -1
- package/dist/scripts/dev-workflow-smoke.js +260 -0
- package/dist/scripts/prepare-client-preview-acceptance.js +65 -0
- package/dist/scripts/verify-agent-contract.js +199 -2
- package/dist/scripts/vibe-auth-open-test-preload.cjs +11 -0
- package/dist/scripts/vibe-ppe-test-preload.cjs +23 -0
- package/dist/scripts/vibe-upload-demo.js +1 -1
- package/dist/scripts/vibe-upload-smoke.js +262 -4
- package/dist/scripts/wasm-smoke.js +53 -0
- package/dist/scripts/wasm-worker.js +63 -0
- package/package.json +11 -9
- package/vibe-upload.md +135 -23
|
@@ -4,6 +4,7 @@ const os = require('node:os');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { spawn } = require('node:child_process');
|
|
6
6
|
const archiver = require('archiver');
|
|
7
|
+
const http = require('node:http');
|
|
7
8
|
|
|
8
9
|
const entry = path.resolve(__dirname, '../dist/index.js');
|
|
9
10
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-vibe-process-'));
|
|
@@ -23,6 +24,11 @@ const input = [
|
|
|
23
24
|
const png =
|
|
24
25
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXyoAAAAASUVORK5CYII=';
|
|
25
26
|
const children = new Set();
|
|
27
|
+
let fixtureServer;
|
|
28
|
+
const requests = [];
|
|
29
|
+
let failStatus = false;
|
|
30
|
+
let webMode = false;
|
|
31
|
+
let webPolls = 0;
|
|
26
32
|
const env = {
|
|
27
33
|
...process.env,
|
|
28
34
|
HOME: temp,
|
|
@@ -31,6 +37,7 @@ const env = {
|
|
|
31
37
|
TTMG_VIBE_PAGE_TTL_MS: '7000',
|
|
32
38
|
TTMG_VIBE_BACKEND: '',
|
|
33
39
|
TTMG_VIBE_API: '',
|
|
40
|
+
TTMG_VIBE_PPE: '',
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
function run(args, onEvent, extraEnv = {}) {
|
|
@@ -94,6 +101,84 @@ function events(result) {
|
|
|
94
101
|
.map(line => JSON.parse(line));
|
|
95
102
|
}
|
|
96
103
|
async function main() {
|
|
104
|
+
// Exercise the built worker IPC and new command without creating a platform task.
|
|
105
|
+
const authWorker = spawn(process.execPath, [entry, '__vibe-page-server'], {
|
|
106
|
+
env: {...env, TTMG_VIBE_PAGE_TTL_MS: '30000'}, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
107
|
+
});
|
|
108
|
+
children.add(authWorker);
|
|
109
|
+
const workerMessage = predicate => new Promise((resolve, reject) => {
|
|
110
|
+
const timer = setTimeout(() => {authWorker.off('message', receive); reject(new Error('worker timeout'));}, 5000);
|
|
111
|
+
function receive(message) {
|
|
112
|
+
if (predicate(message)) {clearTimeout(timer); authWorker.off('message', receive); resolve(message);}
|
|
113
|
+
}
|
|
114
|
+
authWorker.on('message', receive);
|
|
115
|
+
});
|
|
116
|
+
const ready = await workerMessage(message => message.type === 'ready');
|
|
117
|
+
const landing = 'https://developers.tiktok.com/portal_h5/minis_import?task_id=123&import_token=process-private-token';
|
|
118
|
+
let rendered = workerMessage(message => message.type === 'rendered' && message.id === 1);
|
|
119
|
+
authWorker.send({type: 'render', id: 1, html: '<h1>Waiting</h1>', browserLandingUrl: landing});
|
|
120
|
+
await rendered;
|
|
121
|
+
const browserReceipt = path.join(temp, 'browser-open.ndjson');
|
|
122
|
+
const browserEnv = {
|
|
123
|
+
NODE_OPTIONS: `--require=${path.join(__dirname, 'vibe-auth-open-test-preload.cjs')}`,
|
|
124
|
+
TTMG_AUTH_OPEN_TEST_RECEIPT: browserReceipt,
|
|
125
|
+
};
|
|
126
|
+
for (const format of ['json', 'ndjson']) {
|
|
127
|
+
const opened = await run(['auth', 'open', '--page-url', ready.url, '--format', format], undefined, browserEnv);
|
|
128
|
+
assert.equal(opened.code, 0, opened.stderr);
|
|
129
|
+
const result = JSON.parse(opened.stdout);
|
|
130
|
+
assert.equal(result.status, 'open_requested');
|
|
131
|
+
assert.equal(result.platformWrite, false);
|
|
132
|
+
assert.doesNotMatch(opened.stdout + opened.stderr, /import_token|process-private-token/);
|
|
133
|
+
}
|
|
134
|
+
const launches = fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').map(JSON.parse);
|
|
135
|
+
assert.equal(launches.length, 2);
|
|
136
|
+
assert.ok(launches.every(item => item.args.includes(ready.url + '/authorize')));
|
|
137
|
+
const redirect = await fetch(ready.url + '/authorize', {redirect: 'manual'});
|
|
138
|
+
assert.equal(redirect.headers.get('location'), landing);
|
|
139
|
+
assert.equal(redirect.status, 302);
|
|
140
|
+
rendered = workerMessage(message => message.type === 'rendered' && message.id === 2);
|
|
141
|
+
authWorker.send({type: 'render', id: 2, html: '<h1>Complete</h1>'});
|
|
142
|
+
await rendered;
|
|
143
|
+
const stale = await run(['auth', 'open', '--page-url', ready.url, '--format', 'json'], undefined, browserEnv);
|
|
144
|
+
assert.equal(stale.code, 1);
|
|
145
|
+
assert.equal(JSON.parse(stale.stdout).errorCode, 'VIBE_AUTH_PAGE_UNAVAILABLE');
|
|
146
|
+
for (const args of [[], ['--page-url', landing]]) {
|
|
147
|
+
const invalid = await run(['auth', 'open', ...args, '--format', 'json'], undefined, browserEnv);
|
|
148
|
+
assert.equal(invalid.code, 1);
|
|
149
|
+
assert.equal(JSON.parse(invalid.stdout).errorCode, 'VIBE_INVALID_PAGE_URL');
|
|
150
|
+
assert.doesNotMatch(invalid.stdout + invalid.stderr, /process-private-token/);
|
|
151
|
+
}
|
|
152
|
+
assert.equal(fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').length, 2);
|
|
153
|
+
authWorker.kill();
|
|
154
|
+
children.delete(authWorker);
|
|
155
|
+
fixtureServer = http.createServer((req, res) => {
|
|
156
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
157
|
+
requests.push({ path: url.pathname, method: req.method, headers: req.headers });
|
|
158
|
+
req.resume();
|
|
159
|
+
const create = url.pathname.endsWith('/create_minis_import_task_for_agent/');
|
|
160
|
+
const status = url.pathname.endsWith('/get_minis_import_task_status_for_agent/');
|
|
161
|
+
if (create) webPolls = 0;
|
|
162
|
+
if (status && webMode) webPolls++;
|
|
163
|
+
assert.ok(create || status, 'Unexpected real upload endpoint');
|
|
164
|
+
assert.equal(req.headers['x-use-ppe'], '1');
|
|
165
|
+
assert.equal(req.headers['x-tt-env'], 'ppe_op_vc');
|
|
166
|
+
assert.equal(req.headers.cookie, undefined);
|
|
167
|
+
if (status) {
|
|
168
|
+
assert.equal(url.searchParams.get('task_id'), '7684370037737553941');
|
|
169
|
+
assert.equal(url.searchParams.get('import_token'), 'process-test-secret');
|
|
170
|
+
}
|
|
171
|
+
const data = create ? {
|
|
172
|
+
task_id: '7684370037737553941', import_token: 'process-test-secret',
|
|
173
|
+
landing_url: 'https://developers.tiktok.com/portal_h5/minis_import?task_id=7684370037737553941&import_token=process-test-secret',
|
|
174
|
+
poll_interval_seconds: 0.01,
|
|
175
|
+
} : { status: webMode && webPolls >= 5 ? 2 : 1, ready_for_input: webMode && webPolls >= 5 };
|
|
176
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
177
|
+
res.end(JSON.stringify({ ...data, error_info: { code: failStatus && status ? 420500 : 0 } }));
|
|
178
|
+
});
|
|
179
|
+
await new Promise(resolve => fixtureServer.listen(0, '127.0.0.1', resolve));
|
|
180
|
+
env.TTMG_VIBE_TEST_PORT = String(fixtureServer.address().port);
|
|
181
|
+
env.NODE_OPTIONS = `--require ${JSON.stringify(path.join(__dirname, 'vibe-ppe-test-preload.cjs'))}`;
|
|
97
182
|
fs.writeFileSync(icon, Buffer.from(png, 'base64'));
|
|
98
183
|
await new Promise((resolve, reject) => {
|
|
99
184
|
const out = fs.createWriteStream(zip);
|
|
@@ -107,6 +192,77 @@ async function main() {
|
|
|
107
192
|
});
|
|
108
193
|
|
|
109
194
|
let bindingHtml;
|
|
195
|
+
const appInfoPath = path.join(temp, 'app-info.json');
|
|
196
|
+
fs.writeFileSync(appInfoPath, JSON.stringify({
|
|
197
|
+
description: 'Process contract fixture', category: 'Casual', subcategory: 'Puzzle',
|
|
198
|
+
terms_of_service: 'https://example.invalid/terms', privacy_policy: 'https://example.invalid/privacy',
|
|
199
|
+
copyright_confirmation: true,
|
|
200
|
+
}));
|
|
201
|
+
const badIcon = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
|
|
202
|
+
assert.equal(badIcon.code, 1);
|
|
203
|
+
assert.equal(events(badIcon)[0].errorCode, 'VIBE_PLATFORM_ICON_INVALID');
|
|
204
|
+
assert.equal(events(badIcon)[0].platformWrite, false);
|
|
205
|
+
assert.equal(requests.length, 0, 'Invalid input must not create a task');
|
|
206
|
+
assert.equal(fs.existsSync(path.join(temp, '.ttmg/vibe-project.json')), false);
|
|
207
|
+
const platformHeader = Buffer.from(png, 'base64');
|
|
208
|
+
platformHeader.writeUInt32BE(1024, 16);
|
|
209
|
+
platformHeader.writeUInt32BE(1024, 20);
|
|
210
|
+
fs.writeFileSync(icon, platformHeader); // Header preflight fixture, not a decoded image.
|
|
211
|
+
const completeDry = await run([...input, '--app-info', appInfoPath, '--dry-run', '--format', 'json']);
|
|
212
|
+
assert.equal(completeDry.code, 0, completeDry.stderr);
|
|
213
|
+
assert.equal(events(completeDry)[0].status, 'prepared');
|
|
214
|
+
assert.equal(requests.length, 0);
|
|
215
|
+
fs.writeFileSync(appInfoPath, '{private-token');
|
|
216
|
+
const badInfo = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
|
|
217
|
+
assert.equal(events(badInfo)[0].errorCode, 'VIBE_INVALID_APP_INFO');
|
|
218
|
+
assert.doesNotMatch(badInfo.stdout + badInfo.stderr, /private-token/);
|
|
219
|
+
assert.equal(requests.length, 0);
|
|
220
|
+
const legacyInfo = await run(['upload', '--app-info', appInfoPath, '--format', 'json']);
|
|
221
|
+
assert.equal(events(legacyInfo)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
|
|
222
|
+
assert.equal(events(legacyInfo)[0].platformWrite, false);
|
|
223
|
+
fs.writeFileSync(icon, Buffer.from(png, 'base64'));
|
|
224
|
+
// Replay the live package receipt through the built CLI without PPE writes.
|
|
225
|
+
const uploadPaths = [];
|
|
226
|
+
const uploadServer = http.createServer((req, res) => {
|
|
227
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
228
|
+
const endpoint = url.pathname.split('/').filter(Boolean).at(-1);
|
|
229
|
+
uploadPaths.push(endpoint);
|
|
230
|
+
req.resume();
|
|
231
|
+
const replies = {
|
|
232
|
+
create_minis_import_task_for_agent: {
|
|
233
|
+
task_id: '7684370037737619477', import_token: 'receipt-test-secret',
|
|
234
|
+
landing_url: 'https://example.invalid/auth', poll_interval_seconds: 0.01,
|
|
235
|
+
},
|
|
236
|
+
get_minis_import_task_status_for_agent: { status: 2, ready_for_input: true },
|
|
237
|
+
upload_minis_import_app_icon_for_agent: { icon_uri: 'test-icon' },
|
|
238
|
+
submit_minis_import_info_for_agent: {},
|
|
239
|
+
upload_minis_import_package_for_agent: { status: 2, package_uri: 'test-package' },
|
|
240
|
+
};
|
|
241
|
+
res.writeHead(endpoint in replies ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({ error_info: { code: 0 }, ...replies[endpoint] }));
|
|
243
|
+
});
|
|
244
|
+
await new Promise(resolve => uploadServer.listen(0, '127.0.0.1', resolve));
|
|
245
|
+
try {
|
|
246
|
+
const saved = await run([...input, '--format', 'ndjson'], undefined, {
|
|
247
|
+
TTMG_VIBE_BACKEND: 'local-http',
|
|
248
|
+
TTMG_VIBE_API: `http://127.0.0.1:${uploadServer.address().port}`,
|
|
249
|
+
});
|
|
250
|
+
assert.equal(saved.code, 0, saved.stderr);
|
|
251
|
+
const savedEvents = events(saved);
|
|
252
|
+
const savedFinal = savedEvents.at(-1);
|
|
253
|
+
assert.equal(savedFinal.status, 'uploaded', JSON.stringify(savedEvents.map(e => ({ status: e.status, terminal: e.terminal, step: e.uploadStep, uploaded: e.uploaded }))));
|
|
254
|
+
assert.equal(savedFinal.uploaded, true);
|
|
255
|
+
assert.equal(savedFinal.taskStatusRaw, 2);
|
|
256
|
+
assert.equal(savedEvents.filter(e => e.terminal).length, 1);
|
|
257
|
+
assert.deepEqual(uploadPaths, [
|
|
258
|
+
'create_minis_import_task_for_agent', 'get_minis_import_task_status_for_agent',
|
|
259
|
+
'upload_minis_import_app_icon_for_agent', 'submit_minis_import_info_for_agent',
|
|
260
|
+
'upload_minis_import_package_for_agent',
|
|
261
|
+
]);
|
|
262
|
+
assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret|test-package/);
|
|
263
|
+
} finally {
|
|
264
|
+
uploadServer.close();
|
|
265
|
+
}
|
|
110
266
|
const success = await run(
|
|
111
267
|
[...input, '--mock', '--format', 'ndjson', '--poll-interval', '0.2'],
|
|
112
268
|
async event => {
|
|
@@ -117,9 +273,9 @@ async function main() {
|
|
|
117
273
|
assert.equal(success.code, 0, success.stderr);
|
|
118
274
|
assert.match(bindingHtml, /Upload to TikTok/);
|
|
119
275
|
assert.match(bindingHtml, /class="game-avatar"/);
|
|
120
|
-
assert.match(bindingHtml, /Mock
|
|
276
|
+
assert.match(bindingHtml, /PPE \+ Mock/);
|
|
121
277
|
assert.match(bindingHtml, /data-phase="binding"/);
|
|
122
|
-
assert.match(bindingHtml,
|
|
278
|
+
assert.match(bindingHtml, /Phone authorization is not required/);
|
|
123
279
|
assert.doesNotMatch(bindingHtml, /<form|<input/);
|
|
124
280
|
const sequence = events(success);
|
|
125
281
|
const final = sequence[sequence.length - 1];
|
|
@@ -129,7 +285,16 @@ async function main() {
|
|
|
129
285
|
assert.equal(final.nextAction, 'none');
|
|
130
286
|
assert.equal(sequence.filter(e => e.terminal).length, 1);
|
|
131
287
|
assert.equal(final.uploaded, true);
|
|
132
|
-
assert.equal(final.platformWrite,
|
|
288
|
+
assert.equal(final.platformWrite, true);
|
|
289
|
+
assert.equal(final.backend, 'ppe-mock');
|
|
290
|
+
assert.equal(final.taskStatusRaw, 1);
|
|
291
|
+
assert.equal(final.readyForInput, false);
|
|
292
|
+
assert.equal(final.simulatedReadyForInput, true);
|
|
293
|
+
assert.equal(final.simulatedTaskStatusRaw, 3);
|
|
294
|
+
assert.equal(requests.length, 4);
|
|
295
|
+
assert.equal(requests[0].method, 'POST');
|
|
296
|
+
assert.ok(requests.slice(1).every(r => r.method === 'GET'));
|
|
297
|
+
assert.doesNotMatch(success.stdout, /process-test-secret/);
|
|
133
298
|
assert.equal(final.simulation, true);
|
|
134
299
|
const progress = sequence.filter(
|
|
135
300
|
e => e.status === 'uploading' && e.loadedBytes !== undefined,
|
|
@@ -145,10 +310,56 @@ async function main() {
|
|
|
145
310
|
1,
|
|
146
311
|
);
|
|
147
312
|
const previewHtml = await (await fetch(final.pageUrl)).text();
|
|
148
|
-
assert.match(previewHtml, /
|
|
313
|
+
assert.match(previewHtml, /Simulated upload complete/);
|
|
149
314
|
assert.match(previewHtml, /data-phase="complete"/);
|
|
150
315
|
assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
|
|
151
316
|
|
|
317
|
+
const beforeWeb = requests.length;
|
|
318
|
+
const webConflict = await run([...input, '--web', '--mock', '--format', 'json']);
|
|
319
|
+
assert.equal(events(webConflict)[0].errorCode, 'VIBE_WEB_OPTION_CONFLICT');
|
|
320
|
+
const legacyWeb = await run(['upload', '--web', '--format', 'json']);
|
|
321
|
+
assert.equal(events(legacyWeb)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
|
|
322
|
+
assert.equal(requests.length, beforeWeb);
|
|
323
|
+
webMode = true;
|
|
324
|
+
for (const format of ['json', 'ndjson']) {
|
|
325
|
+
const receipt = path.join(temp, `upload-web-${format}.ndjson`);
|
|
326
|
+
const web = await run([...input.filter(v => v !== '--no-open'), '--web', '--format', format, '--poll-interval', '0.2'], async event => {
|
|
327
|
+
if (event.status === 'waiting_authorization') {
|
|
328
|
+
assert.equal(event.authorizationMode, 'web');
|
|
329
|
+
assert.equal(event.browserOpenRequested, true);
|
|
330
|
+
assert.equal(event.simulation, false);
|
|
331
|
+
assert.equal(event.nextAction, 'open_browser');
|
|
332
|
+
assert.equal(event.qrPurpose, undefined);
|
|
333
|
+
const response = await fetch(event.pageUrl + '/authorize', {redirect: 'manual'});
|
|
334
|
+
assert.equal(response.status, 302);
|
|
335
|
+
assert.match(response.headers.get('location'), /^https:\/\/developers\.tiktok\.com\/portal_h5\/minis_import\?/);
|
|
336
|
+
const html = await (await fetch(event.pageUrl)).text();
|
|
337
|
+
assert.match(html, /Authorize in your browser/);
|
|
338
|
+
assert.doesNotMatch(html, /alt="Authorization QR"|Scan the QR code/);
|
|
339
|
+
}
|
|
340
|
+
}, {
|
|
341
|
+
NODE_OPTIONS: env.NODE_OPTIONS + ' --require=' + path.join(__dirname, 'vibe-auth-open-test-preload.cjs'),
|
|
342
|
+
TTMG_AUTH_OPEN_TEST_RECEIPT: receipt,
|
|
343
|
+
// Explicit --web must not inherit an old Mock backend setting.
|
|
344
|
+
TTMG_VIBE_BACKEND: 'mock',
|
|
345
|
+
});
|
|
346
|
+
assert.equal(web.code, 1, web.stderr);
|
|
347
|
+
const output = events(web);
|
|
348
|
+
assert.equal(output.at(-1).errorCode, 'VIBE_REAL_UPLOAD_NOT_READY');
|
|
349
|
+
assert.equal(output.at(-1).simulation, false);
|
|
350
|
+
assert.equal(output.at(-1).backend, 'ppe');
|
|
351
|
+
assert.equal(output.at(-1).readyForInput, true);
|
|
352
|
+
assert.equal(output.at(-1).uploadAttempted, false);
|
|
353
|
+
assert.equal(output.at(-1).uploaded, false);
|
|
354
|
+
assert.equal(webPolls, 5, 'Only true backend readiness unlocks authorization');
|
|
355
|
+
if (format === 'json') assert.equal(output.length, 1);
|
|
356
|
+
const opens = fs.readFileSync(receipt, 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
357
|
+
assert.equal(opens.length, 1);
|
|
358
|
+
assert.ok(opens[0].args.some(arg => /^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{48}\/authorize$/.test(arg)));
|
|
359
|
+
assert.doesNotMatch(JSON.stringify(opens) + web.stdout + web.stderr, /process-test-secret|import_token|aweme:\/\//);
|
|
360
|
+
}
|
|
361
|
+
webMode = false;
|
|
362
|
+
|
|
152
363
|
const json = await run(
|
|
153
364
|
[...input, '--format', 'json', '--poll-interval', '0.01'],
|
|
154
365
|
undefined,
|
|
@@ -160,6 +371,44 @@ async function main() {
|
|
|
160
371
|
assert.equal(JSON.parse(json.stdout).simulation, true);
|
|
161
372
|
assert.match(json.stderr, /http:\/\/127\.0\.0\.1:/);
|
|
162
373
|
|
|
374
|
+
const missingPpe = await run([...input, '--format', 'json'], undefined, {
|
|
375
|
+
TTMG_VIBE_BACKEND: 'ppe',
|
|
376
|
+
});
|
|
377
|
+
assert.equal(missingPpe.code, 1);
|
|
378
|
+
assert.equal(events(missingPpe)[0].errorCode, 'VIBE_INVALID_PPE_ENV');
|
|
379
|
+
assert.equal(events(missingPpe)[0].platformWrite, false);
|
|
380
|
+
const dryPpe = await run([...input, '--dry-run', '--format', 'json'], undefined, {
|
|
381
|
+
TTMG_VIBE_BACKEND: 'ppe', TTMG_VIBE_PPE: 'ppe_op_vc',
|
|
382
|
+
});
|
|
383
|
+
assert.equal(dryPpe.code, 0, dryPpe.stderr);
|
|
384
|
+
assert.equal(events(dryPpe)[0].status, 'prepared');
|
|
385
|
+
assert.equal(events(dryPpe)[0].platformWrite, false);
|
|
386
|
+
const missingHybrid = await run([...input, '--format', 'json'], undefined, {
|
|
387
|
+
TTMG_VIBE_BACKEND: 'ppe-mock',
|
|
388
|
+
});
|
|
389
|
+
assert.equal(events(missingHybrid)[0].errorCode, 'VIBE_INVALID_PPE_ENV');
|
|
390
|
+
assert.equal(events(missingHybrid)[0].platformWrite, false);
|
|
391
|
+
const dryHybrid = await run([...input, '--dry-run', '--format', 'json'], undefined, {
|
|
392
|
+
TTMG_VIBE_BACKEND: 'ppe-mock', TTMG_VIBE_PPE: 'ppe_op_vc',
|
|
393
|
+
});
|
|
394
|
+
assert.equal(dryHybrid.code, 0, dryHybrid.stderr);
|
|
395
|
+
assert.equal(events(dryHybrid)[0].status, 'prepared');
|
|
396
|
+
assert.equal(events(dryHybrid)[0].platformWrite, false);
|
|
397
|
+
const hybridMockOverride = await run([...input, '--mock', '--poll-interval', '0.01', '--format', 'json'], undefined, {
|
|
398
|
+
TTMG_VIBE_BACKEND: 'ppe-mock', TTMG_VIBE_PPE: 'ppe_op_vc',
|
|
399
|
+
});
|
|
400
|
+
assert.equal(hybridMockOverride.code, 0, hybridMockOverride.stderr);
|
|
401
|
+
assert.equal(events(hybridMockOverride)[0].backend, 'ppe-mock');
|
|
402
|
+
assert.equal(events(hybridMockOverride)[0].platformWrite, true);
|
|
403
|
+
|
|
404
|
+
failStatus = true;
|
|
405
|
+
const realFailure = await run([...input, '--mock', '--format', 'json']);
|
|
406
|
+
assert.equal(realFailure.code, 1, realFailure.stderr);
|
|
407
|
+
assert.equal(events(realFailure)[0].errorCode, 'VIBE_BACKEND_REJECTED');
|
|
408
|
+
assert.equal(events(realFailure)[0].uploadAttempted, false);
|
|
409
|
+
assert.equal(events(realFailure)[0].uploaded, false);
|
|
410
|
+
failStatus = false;
|
|
411
|
+
|
|
163
412
|
for (const [scenario, errorCode, attempted] of [
|
|
164
413
|
['denied', 'VIBE_AUTH_DENIED', false],
|
|
165
414
|
['upload-failed', 'VIBE_BACKEND_REQUEST_FAILED', true],
|
|
@@ -193,6 +442,11 @@ async function main() {
|
|
|
193
442
|
assert.equal(prepared.backend, 'none');
|
|
194
443
|
assert.equal(prepared.pageUrl, undefined);
|
|
195
444
|
assert.equal(prepared.uploadAttempted, false);
|
|
445
|
+
const beforeDryMock = requests.length;
|
|
446
|
+
const dryMock = await run([...input, '--mock', '--dry-run', '--format', 'json']);
|
|
447
|
+
assert.equal(dryMock.code, 0);
|
|
448
|
+
assert.equal(events(dryMock)[0].platformWrite, false);
|
|
449
|
+
assert.equal(requests.length, beforeDryMock);
|
|
196
450
|
|
|
197
451
|
const project = path.join(temp, 'project');
|
|
198
452
|
fs.mkdirSync(project);
|
|
@@ -228,6 +482,9 @@ async function main() {
|
|
|
228
482
|
assert.equal(help.code, 0);
|
|
229
483
|
assert.match(help.stdout, /--vibe/);
|
|
230
484
|
assert.match(help.stdout, /--client-key/);
|
|
485
|
+
const authHelp = await run(['auth', 'open', '--help']);
|
|
486
|
+
assert.equal(authHelp.code, 0);
|
|
487
|
+
assert.match(authHelp.stdout, /--page-url/);
|
|
231
488
|
|
|
232
489
|
let cancelledPage;
|
|
233
490
|
const cancelled = await run(
|
|
@@ -254,5 +511,6 @@ main()
|
|
|
254
511
|
})
|
|
255
512
|
.finally(() => {
|
|
256
513
|
for (const child of children) child.kill('SIGKILL');
|
|
514
|
+
fixtureServer?.close();
|
|
257
515
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
258
516
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { spawnSync } = require('node:child_process');
|
|
7
|
+
const entry = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, '../dist/index.js');
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-wasm-smoke-'));
|
|
9
|
+
const input = path.join(root, 'original.wasm');
|
|
10
|
+
const wasm = Buffer.from('0061736d010000000105016000017f0304030000000404017000030707010372756e00000909010041000b030001020a1003040010010b0400412a0b040041070b', 'hex');
|
|
11
|
+
const sha = crypto.createHash('sha256').update(wasm).digest('hex');
|
|
12
|
+
function run(args, env = {}) {
|
|
13
|
+
const result = spawnSync(process.execPath, [entry, ...args, '--format', 'json'], {
|
|
14
|
+
cwd: root, encoding: 'utf8', timeout: 15000,
|
|
15
|
+
env: { ...process.env, TTMGRC_PATH: path.join(root, 'rc.json'), ...env },
|
|
16
|
+
});
|
|
17
|
+
assert.ifError(result.error);
|
|
18
|
+
assert.equal(result.stdout.trim().split('\n').length, 1, result.stdout + result.stderr);
|
|
19
|
+
const body = JSON.parse(result.stdout);
|
|
20
|
+
assert.equal(result.status, body.status === 'failed' ? 1 : 0, result.stderr);
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(input, wasm);
|
|
25
|
+
const stub = path.join(root, 'missing-binding.js');
|
|
26
|
+
fs.writeFileSync(stub, "const Module=require('node:module'); const load=Module._load; Module._load=function(name,...args){if(name==='@anrans001/ttmg-wasmtool')throw new Error('fixture unavailable');return load.call(this,name,...args)};");
|
|
27
|
+
const unavailable = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'missing')], { NODE_OPTIONS: '--require=' + stub });
|
|
28
|
+
assert.equal(unavailable.errorCode, 'WASM_BINDING_UNAVAILABLE');
|
|
29
|
+
assert.equal(fs.existsSync(path.join(root, 'missing')), false);
|
|
30
|
+
const preflight = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'dry'), '--dry-run'], { NODE_OPTIONS: '--require=' + stub });
|
|
31
|
+
assert.equal(preflight.status, 'ready');
|
|
32
|
+
assert.equal(preflight.nativeBindingChecked, false);
|
|
33
|
+
assert.equal(fs.existsSync(path.join(root, 'dry')), false);
|
|
34
|
+
const collection = path.join(root, 'collection.json');
|
|
35
|
+
fs.writeFileSync(collection, JSON.stringify({ schemaVersion: '1', wasmSha256: sha, funcIds: [0, 1], bootFuncIds: [0] }));
|
|
36
|
+
const prepared = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'prepared')]);
|
|
37
|
+
if (prepared.errorCode === 'WASM_BINDING_UNAVAILABLE') {
|
|
38
|
+
console.log('wasm process contract passed; native execution unavailable on this host, not verified');
|
|
39
|
+
} else {
|
|
40
|
+
assert.equal(prepared.status, 'succeeded', JSON.stringify(prepared));
|
|
41
|
+
const split = run(['wasm', 'split', '--input', input, '--collection', collection, '--output', path.join(root, 'split')]);
|
|
42
|
+
assert.equal(split.status, 'succeeded', JSON.stringify(split));
|
|
43
|
+
assert.equal(split.detail.totalWasmCount, 3);
|
|
44
|
+
assert.equal(split.detail.mainWasmCount, 2);
|
|
45
|
+
assert.equal(split.gameValidation, 'not_evaluated');
|
|
46
|
+
assert.equal(split.input.wasmSha256, sha);
|
|
47
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(split.manifest, 'utf8')), split);
|
|
48
|
+
for (const artifact of split.artifacts) assert.equal(crypto.createHash('sha256').update(fs.readFileSync(artifact.path)).digest('hex'), artifact.sha256);
|
|
49
|
+
assert.deepEqual(fs.readFileSync(input), wasm);
|
|
50
|
+
assert.equal(fs.existsSync(path.join(root, 'rc.json')), false);
|
|
51
|
+
console.log(`wasm process contract passed: native prepare/split on ${process.platform}/${process.arch}, immutable input and verified artifacts`);
|
|
52
|
+
}
|
|
53
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Native Wasm operations run in a disposable process so a deadline can stop
|
|
2
|
+
// synchronous native code without blocking or corrupting the CLI owner.
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const zlib = require('node:zlib');
|
|
6
|
+
|
|
7
|
+
process.once('message', request => {
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
let binding;
|
|
11
|
+
try {
|
|
12
|
+
binding = require('@anrans001/ttmg-wasmtool');
|
|
13
|
+
} catch {
|
|
14
|
+
throw Object.assign(new Error('Wasm tool binding is unavailable for this OS/architecture.'), {
|
|
15
|
+
code: 'WASM_BINDING_UNAVAILABLE',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (request.operation === 'prepare') {
|
|
19
|
+
const output = path.join(request.output, 'prepared.wasm');
|
|
20
|
+
const result = binding.prepare(request.input, output);
|
|
21
|
+
if (!fs.existsSync(output) || result.outputSize <= 0)
|
|
22
|
+
throw new Error('Wasm preparation produced no output.');
|
|
23
|
+
fs.writeFileSync(output + '.br', zlib.brotliCompressSync(fs.readFileSync(output), {
|
|
24
|
+
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 9 },
|
|
25
|
+
}));
|
|
26
|
+
response = { ok: true, detail: { outputSize: result.outputSize, timeCost: result.timeCost } };
|
|
27
|
+
} else if (request.operation === 'split') {
|
|
28
|
+
const result = binding.split({
|
|
29
|
+
input: request.input,
|
|
30
|
+
outputDir: request.output,
|
|
31
|
+
funcIds: request.funcIds,
|
|
32
|
+
alwaysInclude: request.bootFuncIds,
|
|
33
|
+
archive: true,
|
|
34
|
+
compress: true,
|
|
35
|
+
quality: 9,
|
|
36
|
+
callClosure: true,
|
|
37
|
+
callIndirectClosure: false,
|
|
38
|
+
});
|
|
39
|
+
if (result.code !== 0) throw new Error(result.errMsg || 'Wasm split failed.');
|
|
40
|
+
if (![...request.funcIds, ...request.bootFuncIds].every(id => id < result.totalWasmCount))
|
|
41
|
+
throw Object.assign(new Error('Collection contains function IDs outside this Wasm module.'), {
|
|
42
|
+
code: 'WASM_COLLECTION_INVALID',
|
|
43
|
+
});
|
|
44
|
+
const detail = {};
|
|
45
|
+
for (const key of ['totalWasmCount', 'mainWasmCount', 'tableSize', 'timeCost',
|
|
46
|
+
'globalVarList', 'collectFuncCount', 'alwaysIncludeAdded', 'closureAdded',
|
|
47
|
+
'indirectClosureAdded', 'indirectClosureTypes', 'exportAdded']) detail[key] = result[key];
|
|
48
|
+
detail.outputFiles = [result.mainWasmPath, result.subWasmPath, result.funcMetaPath, result.archivePath]
|
|
49
|
+
.filter(Boolean).map(file => {
|
|
50
|
+
if (path.dirname(path.resolve(file)) !== path.resolve(request.output))
|
|
51
|
+
throw new Error('Native output escaped its assigned directory.');
|
|
52
|
+
return path.basename(file);
|
|
53
|
+
});
|
|
54
|
+
response = { ok: true, detail };
|
|
55
|
+
} else throw new Error('Unknown Wasm operation.');
|
|
56
|
+
response.detail.bindingVersion = require('@anrans001/ttmg-wasmtool/package.json').version;
|
|
57
|
+
response.detail.platform = process.platform;
|
|
58
|
+
response.detail.arch = process.arch;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
response = { ok: false, errorCode: error.code || 'WASM_OPERATION_FAILED', error: error.message };
|
|
61
|
+
}
|
|
62
|
+
process.send(response, () => process.disconnect());
|
|
63
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttmg/cli",
|
|
3
|
-
"version": "0.4.6-vibe-beta.
|
|
3
|
+
"version": "0.4.6-vibe-beta.8",
|
|
4
4
|
"description": "TikTok Mini Game Command Line Tool",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"bin": {
|
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
"test:auth": "node scripts/run-ts-test.js src/commands/common/auth.test.ts",
|
|
32
32
|
"test:capabilities": "node scripts/run-ts-test.js src/commands/common/capabilities.test.ts",
|
|
33
33
|
"test:test-account": "node scripts/run-ts-test.js src/commands/common/testAccount.test.ts",
|
|
34
|
-
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts",
|
|
35
|
-
"test:
|
|
34
|
+
"test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts && npm run test:packaging-compatibility",
|
|
35
|
+
"test:packaging-compatibility": "node scripts/run-ts-test.js src/commands/native/dev/packagingCompatibility.test.ts",
|
|
36
|
+
"test:dev-session-command": "node scripts/run-ts-test.js src/commands/native/dev/session.test.ts && node scripts/run-ts-test.js src/commands/native/dev/projectSession.test.ts && node scripts/run-ts-test.js src/commands/native/dev/scanPage.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/logArchive.test.ts && node scripts/run-ts-test.js src/commands/native/dev/capture.test.ts",
|
|
36
37
|
"test:dev-runtime-event": "node scripts/run-ts-test.js src/commands/native/dev/utils/scoket.test.ts",
|
|
37
38
|
"test:dev-runtime-client": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtime.test.ts",
|
|
38
|
-
"test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts",
|
|
39
|
+
"test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/previewWorkflow.test.ts",
|
|
39
40
|
"test:dev-runtime-session-follow": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtimeSessionFollow.test.ts",
|
|
40
41
|
"test:dev-client-preview-orchestrator": "node scripts/run-ts-test.js src/commands/native/dev/runtime/clientPreviewOrchestrator.test.ts",
|
|
41
42
|
"test:dev-watch-change": "node scripts/run-ts-test.js src/commands/native/dev/utils/watchChange.test.ts",
|
|
@@ -46,9 +47,10 @@
|
|
|
46
47
|
"test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
|
|
47
48
|
"test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
|
|
48
49
|
"test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
|
|
49
|
-
"test:
|
|
50
|
-
"test:agent-contract
|
|
51
|
-
"
|
|
50
|
+
"test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
|
|
51
|
+
"test:agent-contract": "npm run test:login && npm run test:auth && npm run test:capabilities && npm run test:test-account && npm run test:upload-preview && npm run test:preview-page && npm run test:check && npm run test:check-ai && npm run test:build-structured && npm run test:doctor && npm run test:wasm && npm run test:dev-session && npm run test:dev-session-command && npm run test:dev-runtime-event && npm run test:dev-runtime-client && npm run test:dev-session-control && npm run test:dev-runtime-session-follow && npm run test:dev-client-preview-orchestrator && npm run test:dev-watch-change && npm run test:network && npm run test:vibe && npx tsc --noEmit && npm run build && npm run test:vibe-process && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
|
|
52
|
+
"test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process && npm run test:dev-client-preview-control-process && node scripts/dev-workflow-smoke.js",
|
|
53
|
+
"publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag vibe-beta",
|
|
52
54
|
"setup": "node scripts/setup.js",
|
|
53
55
|
"reset:local-state": "node scripts/resetLocalState.js",
|
|
54
56
|
"dev:link": "npm run build && npm link",
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
],
|
|
61
63
|
"author": "zhanghongyang.mocha",
|
|
62
64
|
"dependencies": {
|
|
63
|
-
"@anrans001/ttmg-wasmtool": "^0.3.
|
|
65
|
+
"@anrans001/ttmg-wasmtool": "^0.3.3",
|
|
64
66
|
"acorn": "^8.15.0",
|
|
65
67
|
"archiver": "^7.0.1",
|
|
66
68
|
"axios": "^1.11.0",
|
|
@@ -93,7 +95,7 @@
|
|
|
93
95
|
"qs": "6.12.1",
|
|
94
96
|
"semver": "^7.7.2",
|
|
95
97
|
"socks-proxy-agent": "^9.0.0",
|
|
96
|
-
"ttmg-pack": "0.4.13
|
|
98
|
+
"ttmg-pack": "0.4.13",
|
|
97
99
|
"ws": "^8.18.3"
|
|
98
100
|
},
|
|
99
101
|
"devDependencies": {
|