@ttmg/cli 0.4.6-vibe-beta.22 → 0.4.6-vibe-beta.23
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/CHANGELOG.md +516 -0
- package/README.md +5 -13
- package/dist/index.js +194 -529
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/scripts/vibe-upload-smoke.js +29 -23
- package/package.json +1 -1
- package/src/commands/common/game/categories.test.ts +38 -0
- package/src/commands/common/game/categories.ts +48 -0
- package/src/commands/common/game/categoryCatalog.json +522 -0
- package/vibe-upload.md +18 -27
package/dist/package.json
CHANGED
|
@@ -115,7 +115,7 @@ function events(result) {
|
|
|
115
115
|
async function main() {
|
|
116
116
|
// Exercise the built worker IPC and new command without creating a platform task.
|
|
117
117
|
const authWorker = spawn(process.execPath, [entry, '__vibe-page-server'], {
|
|
118
|
-
env: {...env, TTMG_VIBE_PAGE_TTL_MS: '
|
|
118
|
+
env: {...env, TTMG_VIBE_PAGE_TTL_MS: '30000'}, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
119
119
|
});
|
|
120
120
|
children.add(authWorker);
|
|
121
121
|
const workerMessage = predicate => new Promise((resolve, reject) => {
|
|
@@ -130,9 +130,6 @@ async function main() {
|
|
|
130
130
|
let rendered = workerMessage(message => message.type === 'rendered' && message.id === 1);
|
|
131
131
|
authWorker.send({type: 'render', id: 1, html: '<h1>Waiting</h1>', browserLandingUrl: landing});
|
|
132
132
|
await rendered;
|
|
133
|
-
await new Promise(resolve => setTimeout(resolve, 700));
|
|
134
|
-
assert.equal(await (await fetch(ready.url)).text(), '<h1>Waiting</h1>',
|
|
135
|
-
'Retention TTL must not close the page while the CLI is still polling');
|
|
136
133
|
const browserReceipt = path.join(temp, 'browser-open.ndjson');
|
|
137
134
|
const browserEnv = {
|
|
138
135
|
NODE_OPTIONS: `--require=${path.join(__dirname, 'vibe-auth-open-test-preload.cjs')}`,
|
|
@@ -165,15 +162,7 @@ async function main() {
|
|
|
165
162
|
assert.doesNotMatch(invalid.stdout + invalid.stderr, /process-private-token/);
|
|
166
163
|
}
|
|
167
164
|
assert.equal(fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').length, 2);
|
|
168
|
-
|
|
169
|
-
const timer = setTimeout(() => reject(new Error('Detached worker did not expire')), 4000);
|
|
170
|
-
authWorker.once('exit', code => { clearTimeout(timer); resolve(code); });
|
|
171
|
-
});
|
|
172
|
-
authWorker.disconnect();
|
|
173
|
-
assert.equal(await (await fetch(ready.url)).text(), '<h1>Complete</h1>',
|
|
174
|
-
'A completed page remains readable after CLI detaches');
|
|
175
|
-
assert.equal(await workerExited, 0);
|
|
176
|
-
await assert.rejects(fetch(ready.url), 'Detached page must close when retention expires');
|
|
165
|
+
authWorker.kill();
|
|
177
166
|
children.delete(authWorker);
|
|
178
167
|
fixtureServer = http.createServer(async (req, res) => {
|
|
179
168
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
@@ -319,7 +308,8 @@ async function main() {
|
|
|
319
308
|
const replies = {
|
|
320
309
|
create_minis_import_task_for_agent: {
|
|
321
310
|
task_id: '7684370037737619477', import_token: 'receipt-test-secret',
|
|
322
|
-
landing_url: 'https://example.invalid/auth',
|
|
311
|
+
landing_url: 'https://example.invalid/auth?task_id=7684370037737619477&import_token=receipt-test-secret',
|
|
312
|
+
poll_interval_seconds: 0.01,
|
|
323
313
|
},
|
|
324
314
|
get_minis_import_task_detail_for_agent: { status: 2, ready_for_input: true, preview_url: 'https://www.tiktok.com/minis/receipt-preview' },
|
|
325
315
|
upload_minis_import_app_icon_for_agent: { icon_uri: 'test-icon' },
|
|
@@ -331,14 +321,35 @@ async function main() {
|
|
|
331
321
|
});
|
|
332
322
|
await new Promise(resolve => uploadServer.listen(0, '127.0.0.1', resolve));
|
|
333
323
|
try {
|
|
334
|
-
const saved = await run([...input, '--format', 'ndjson'], undefined, {
|
|
324
|
+
const saved = await run([...input, '--format', 'ndjson', '--verbose'], undefined, {
|
|
335
325
|
TTMG_VIBE_BACKEND: 'local-http',
|
|
336
326
|
TTMG_VIBE_API: `http://127.0.0.1:${uploadServer.address().port}`,
|
|
337
327
|
});
|
|
338
328
|
assert.equal(saved.code, 0, saved.stderr);
|
|
339
329
|
const savedEvents = events(saved);
|
|
340
330
|
const waitingPage = savedEvents.find(e => e.status === 'waiting_authorization').pageUrl;
|
|
341
|
-
|
|
331
|
+
const savedStderr = saved.stderr.split(/\r?\n/).filter(Boolean);
|
|
332
|
+
assert.deepEqual(savedStderr.filter(line => line.startsWith('page: ')), [`page: ${waitingPage}`]);
|
|
333
|
+
const httpTraces = savedStderr.filter(line => line.startsWith('{')).map(JSON.parse);
|
|
334
|
+
assert.deepEqual(httpTraces.map(trace => trace.event), [
|
|
335
|
+
'http_request', 'http_response',
|
|
336
|
+
'http_request', 'http_response',
|
|
337
|
+
'http_request', 'http_response',
|
|
338
|
+
'http_request', 'http_response',
|
|
339
|
+
'http_request', 'http_response',
|
|
340
|
+
'http_request', 'http_response',
|
|
341
|
+
]);
|
|
342
|
+
const createRequest = httpTraces.find(trace => trace.event === 'http_request' && trace.url.endsWith('/create_minis_import_task_for_agent/'));
|
|
343
|
+
const createResponse = httpTraces.find(trace => trace.event === 'http_response' && trace.url.endsWith('/create_minis_import_task_for_agent/'));
|
|
344
|
+
assert.equal(createRequest.request.source_project_id.length, 36);
|
|
345
|
+
assert.equal(createRequest.request.task_idempotency_id.length, 36);
|
|
346
|
+
assert.equal(createResponse.response.task_id, '7684370037737619477');
|
|
347
|
+
assert.equal(createResponse.response.import_token, '[REDACTED]');
|
|
348
|
+
assert.match(createResponse.response.landing_url, /import_token=%5BREDACTED%5D/);
|
|
349
|
+
const pollRequests = httpTraces.filter(trace => trace.event === 'http_request' && trace.url.endsWith('/get_minis_import_task_detail_for_agent/'));
|
|
350
|
+
assert.ok(pollRequests.length >= 2);
|
|
351
|
+
assert.ok(pollRequests.every(trace => trace.request.task_id === '7684370037737619477' && trace.request.import_token === '[REDACTED]'));
|
|
352
|
+
assert.doesNotMatch(saved.stderr, /receipt-test-secret/);
|
|
342
353
|
const savedFinal = savedEvents.at(-1);
|
|
343
354
|
assert.equal(savedFinal.status, 'uploaded', JSON.stringify(savedEvents.map(e => ({ status: e.status, terminal: e.terminal, step: e.uploadStep, uploaded: e.uploaded }))));
|
|
344
355
|
assert.equal(savedFinal.uploaded, true);
|
|
@@ -350,7 +361,7 @@ async function main() {
|
|
|
350
361
|
'upload_minis_import_package_for_agent',
|
|
351
362
|
'get_minis_import_task_detail_for_agent',
|
|
352
363
|
]);
|
|
353
|
-
assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret
|
|
364
|
+
assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret/);
|
|
354
365
|
} finally {
|
|
355
366
|
uploadServer.close();
|
|
356
367
|
}
|
|
@@ -403,10 +414,7 @@ async function main() {
|
|
|
403
414
|
const previewHtml = await (await fetch(final.pageUrl)).text();
|
|
404
415
|
assert.match(previewHtml, /Simulated upload complete/);
|
|
405
416
|
assert.match(previewHtml, /data-phase="complete"/);
|
|
406
|
-
assert.doesNotMatch(previewHtml
|
|
407
|
-
const localizedPage = await fetch(final.pageUrl, {headers: {'Accept-Language': 'zh-CN'}});
|
|
408
|
-
assert.equal(localizedPage.headers.get('vary'), 'Accept-Language');
|
|
409
|
-
assert.match(await localizedPage.text(), /<html lang="zh-CN">[\s\S]*模拟上传完成/);
|
|
417
|
+
assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
|
|
410
418
|
|
|
411
419
|
const beforeWeb = requests.length;
|
|
412
420
|
const webConflict = await run([...input, '--web', '--mock', '--format', 'json']);
|
|
@@ -495,9 +503,7 @@ async function main() {
|
|
|
495
503
|
assert.equal(output.at(-1).codeAssetId, '7686101169281894418');
|
|
496
504
|
assert.equal(output.at(-1).qrPurpose, 'game_preview');
|
|
497
505
|
const readyPage = await (await fetch(output.at(-1).pageUrl)).text();
|
|
498
|
-
assert.match(readyPage, /Progress QR/);
|
|
499
506
|
assert.match(readyPage, /Game preview QR/);
|
|
500
|
-
assert.ok(readyPage.indexOf('alt="Progress QR"') < readyPage.indexOf('alt="Game preview QR"'));
|
|
501
507
|
assert.match(readyPage, /data-phase="preview"/);
|
|
502
508
|
assert.equal(output.filter(e => e.terminal).length, 1);
|
|
503
509
|
assert.equal(webPolls, 7, 'Keep polling the same task until preview URL is available');
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { GAME_CATEGORIES, GAME_CATEGORY_CATALOG_VERSION, getGameCategories, isGameCategoryPair } from './categories';
|
|
4
|
+
import { applyVibeCategory, VIBE_PPE_DEFAULT_APP_INFO } from '../vibe/appInfo';
|
|
5
|
+
|
|
6
|
+
const result = getGameCategories({ format: 'json' });
|
|
7
|
+
assert.equal(result.platformWrite, false);
|
|
8
|
+
assert.equal(result.categoryCount, 12);
|
|
9
|
+
assert.equal(result.subcategoryCount, 114);
|
|
10
|
+
assert.equal(result.errorCode, null);
|
|
11
|
+
assert.equal(GAME_CATEGORY_CATALOG_VERSION, 'sha256:' + createHash('sha256').update(JSON.stringify(GAME_CATEGORIES)).digest('hex'));
|
|
12
|
+
assert.equal(new Set(GAME_CATEGORIES.map(item => item.value)).size, 12);
|
|
13
|
+
for (const category of GAME_CATEGORIES) {
|
|
14
|
+
assert.equal(new Set(category.subcategories.map(item => item.value)).size, category.subcategories.length);
|
|
15
|
+
for (const item of category.subcategories) assert.ok(item.description.trim());
|
|
16
|
+
}
|
|
17
|
+
assert.deepEqual(getGameCategories({ category: 'Puzzle' }).categories.map(item => item.value), ['Puzzle']);
|
|
18
|
+
assert.equal(getGameCategories({ category: 'puzzle' }).errorCode, 'GAME_CATEGORY_UNKNOWN');
|
|
19
|
+
assert.equal(getGameCategories({ category: '' }).errorCode, 'GAME_CATEGORY_UNKNOWN');
|
|
20
|
+
assert.equal(getGameCategories({ format: 'xml' }).errorCode, 'GAME_CATEGORIES_INVALID_FORMAT');
|
|
21
|
+
assert.ok(isGameCategoryPair('Action', "Shoot'em Up"));
|
|
22
|
+
assert.ok(isGameCategoryPair('Action', "Beat 'em up"));
|
|
23
|
+
assert.ok(!isGameCategoryPair('Other', 'Physics'));
|
|
24
|
+
assert.ok(!isGameCategoryPair('Casino', 'Bingo'));
|
|
25
|
+
|
|
26
|
+
assert.equal(applyVibeCategory({}), undefined);
|
|
27
|
+
const legacy = { ...VIBE_PPE_DEFAULT_APP_INFO, category: 'Casual', subcategory: 'Puzzle', description: 'Legacy description' };
|
|
28
|
+
assert.equal(applyVibeCategory({}, legacy), legacy, 'Old file callers remain unchanged when new flags are absent');
|
|
29
|
+
const pair = { category: 'Puzzle', subcategory: 'Physics' };
|
|
30
|
+
assert.deepEqual(applyVibeCategory(pair), { ...VIBE_PPE_DEFAULT_APP_INFO, ...pair });
|
|
31
|
+
assert.deepEqual(applyVibeCategory(pair, legacy), { ...legacy, ...pair });
|
|
32
|
+
assert.equal(VIBE_PPE_DEFAULT_APP_INFO.category, 'Other');
|
|
33
|
+
assert.equal(legacy.category, 'Casual', 'Overrides must not mutate old input');
|
|
34
|
+
for (const options of [{category:'Puzzle'}, {subcategory:'Physics'}])
|
|
35
|
+
assert.throws(() => applyVibeCategory(options), /VIBE_CATEGORY_PAIR_REQUIRED/);
|
|
36
|
+
for (const options of [{category:'Other',subcategory:'Physics'}, {category:'puzzle',subcategory:'Physics'}, {category:'',subcategory:''}])
|
|
37
|
+
assert.throws(() => applyVibeCategory(options), /VIBE_INVALID_CATEGORY/);
|
|
38
|
+
console.log('game category catalog and compatibility tests passed');
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import catalog from './categoryCatalog.json';
|
|
3
|
+
import { t } from '../../../libs/i18n';
|
|
4
|
+
|
|
5
|
+
export const GAME_CATEGORY_CATALOG_VERSION = catalog.catalogVersion;
|
|
6
|
+
export const GAME_CATEGORIES = catalog.categories;
|
|
7
|
+
|
|
8
|
+
export function isGameCategoryPair(category: string, subcategory: string) {
|
|
9
|
+
return GAME_CATEGORIES.some(item => item.value === category &&
|
|
10
|
+
item.subcategories.some(child => child.value === subcategory));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getGameCategories(options: { category?: string; format?: string } = {}) {
|
|
14
|
+
const errorCode = !['text', 'json', 'ndjson'].includes(options.format || 'text')
|
|
15
|
+
? 'GAME_CATEGORIES_INVALID_FORMAT'
|
|
16
|
+
: options.category !== undefined && !GAME_CATEGORIES.some(item => item.value === options.category)
|
|
17
|
+
? 'GAME_CATEGORY_UNKNOWN' : null;
|
|
18
|
+
const categories = errorCode ? [] : GAME_CATEGORIES.filter(item =>
|
|
19
|
+
options.category === undefined || item.value === options.category);
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: '1',
|
|
22
|
+
command: 'game categories',
|
|
23
|
+
operationId: `game_categories_${randomUUID()}`,
|
|
24
|
+
status: errorCode ? 'failed' : 'ready',
|
|
25
|
+
terminal: true,
|
|
26
|
+
platformWrite: false,
|
|
27
|
+
catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
|
|
28
|
+
source: catalog.source,
|
|
29
|
+
descriptionLanguage: 'en',
|
|
30
|
+
categoryCount: categories.length,
|
|
31
|
+
subcategoryCount: categories.reduce((count, item) => count + item.subcategories.length, 0),
|
|
32
|
+
categories,
|
|
33
|
+
errorCode,
|
|
34
|
+
error: errorCode ? t(errorCode === 'GAME_CATEGORY_UNKNOWN'
|
|
35
|
+
? 'cli.command.game.categories.unknown' : 'cli.command.game.categories.invalidFormat') : null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function gameCategories(options: { category?: string; format?: string }) {
|
|
40
|
+
const result = getGameCategories(options);
|
|
41
|
+
if (options.format && options.format !== 'text') console.log(JSON.stringify(result));
|
|
42
|
+
else if (result.error) console.error(result.error);
|
|
43
|
+
else for (const category of result.categories) {
|
|
44
|
+
console.log(category.value);
|
|
45
|
+
for (const item of category.subcategories) console.log(` ${item.value}: ${item.description}`);
|
|
46
|
+
}
|
|
47
|
+
if (result.errorCode) process.exitCode = 1;
|
|
48
|
+
}
|