@ttmg/cli 0.4.5 → 0.4.6-agent-beta.1
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 +644 -2
- package/dist/index.js +22502 -6070
- package/dist/index.js.map +1 -1
- package/dist/package.json +36 -4
- package/dist/public/assets/{Card-BAsimNoh.js → Card-CN2lxY0Y.js} +1 -1
- package/dist/public/assets/{Detail-rVxbEDgh.js → Detail-CELZZ-4h.js} +1 -1
- package/dist/public/assets/Detail-CELZZ-4h.js.br +0 -0
- package/dist/public/assets/{MonetizationMode-B20TAE9o.js → MonetizationMode-C7DNqium.js} +1 -1
- package/dist/public/assets/{MonetizationModeSummary-CnHqXyco.js → MonetizationModeSummary-DYE7XUq1.js} +1 -1
- package/dist/public/assets/{SectionHeader-Cn3FnOqh.js → SectionHeader-BKc3BWXP.js} +1 -1
- package/dist/public/assets/{baseForm-kBTxn8h8.js → baseForm-I9suIiQn.js} +1 -1
- package/dist/public/assets/baseForm-I9suIiQn.js.br +0 -0
- package/dist/public/assets/{col-VN3Ivd-x.js → col-w9Q-2mgM.js} +1 -1
- package/dist/public/assets/{compass-BabZVjAl.js → compass-D6waJ52O.js} +1 -1
- package/dist/public/assets/{index-DHdJsD10.js → index-8sDAOAcS.js} +1 -1
- package/dist/public/assets/index-8sDAOAcS.js.br +0 -0
- package/dist/public/assets/{index-DK4IvdRl.js → index-B6hY5GdS.js} +1 -1
- package/dist/public/assets/{index-Cl0_vMWE.js → index-BIrFkB6r.js} +1 -1
- package/dist/public/assets/index-BIrFkB6r.js.br +0 -0
- package/dist/public/assets/{index-BjQlJrX7.js → index-BLWgpW64.js} +1 -1
- package/dist/public/assets/{index-odEI7YQF.js → index-BY3eQmfP.js} +1 -1
- package/dist/public/assets/index-BY3eQmfP.js.br +0 -0
- package/dist/public/assets/{index-Bjn0ctEd.js → index-BegU9LBT.js} +2 -2
- package/dist/public/assets/index-BegU9LBT.js.br +0 -0
- package/dist/public/assets/{index-1WjGwqIV.js → index-BqwflQml.js} +1 -1
- package/dist/public/assets/{index-DXDZCuG4.js → index-BrOFdVbv.js} +1 -1
- package/dist/public/assets/{index-BxsB5E3u.js → index-BvPolBSn.js} +1 -1
- package/dist/public/assets/{index-BnOeYQAd.js → index-BwrZjkCc.js} +1 -1
- package/dist/public/assets/{index-CUuX90R9.js → index-CLKKui8G.js} +1 -1
- package/dist/public/assets/{index-Dohibgdp.js → index-Cb4Ja0tZ.js} +1 -1
- package/dist/public/assets/{index-C83QHLmD.js → index-CcUgN29Y.js} +1 -1
- package/dist/public/assets/{index-aSl6Wdrp.js → index-CgXOkYrX.js} +1 -1
- package/dist/public/assets/index-CgXOkYrX.js.br +0 -0
- package/dist/public/assets/{index-DulANwbq.js → index-CsiIrLKY.js} +1 -1
- package/dist/public/assets/{index-Ba-JWe4X.js → index-Csmg5oyu.js} +1 -1
- package/dist/public/assets/{index-DnnfZ7H_.js → index-DDecPuod.js} +1 -1
- package/dist/public/assets/{index-Clq0hTDI.js → index-DDy4PkWT.js} +1 -1
- package/dist/public/assets/{index-DIcz5ciL.js → index-DEQ2hVPA.js} +1 -1
- package/dist/public/assets/index-DEQ2hVPA.js.br +0 -0
- package/dist/public/assets/{index-CpM8t743.js → index-DGkgqtgJ.js} +1 -1
- package/dist/public/assets/{index-QNLvejKK.js → index-FVHabbDB.js} +1 -1
- package/dist/public/assets/{index-CzXMNZL3.js → index-M8jf-YAZ.js} +1 -1
- package/dist/public/assets/{index-BxokHznc.js → index-Q0GC8Irj.js} +1 -1
- package/dist/public/assets/index-Q0GC8Irj.js.br +0 -0
- package/dist/public/assets/{index--6iHQzho.js → index-YVB9ZW00.js} +1 -1
- package/dist/public/assets/{index-O_L1tU2b.js → index-mjEeEQcC.js} +1 -1
- package/dist/public/assets/index-mjEeEQcC.js.br +0 -0
- package/dist/public/assets/{index-BFFsJ47B.js → index-rLkONFpx.js} +1 -1
- package/dist/public/assets/{layers-B4zhx14P.js → layers-Dbwq9AyL.js} +1 -1
- package/dist/public/assets/{list-checks-Br6imjXC.js → list-checks-og1usOZf.js} +1 -1
- package/dist/public/assets/{sparkles-BjIkcbAi.js → sparkles-p-dZbjGb.js} +1 -1
- package/dist/public/assets/{triangle-alert-CgKAc3w5.js → triangle-alert-BZVAEUiL.js} +1 -1
- package/dist/public/assets/{zap-DFHw5liD.js → zap-BAhZUPPa.js} +1 -1
- package/dist/public/index.html +1 -1
- 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 +606 -0
- package/dist/scripts/dev-default-smoke.js +67 -0
- package/dist/scripts/dev-game-ready-smoke.js +155 -0
- package/dist/scripts/dev-workflow-smoke.js +260 -0
- package/dist/scripts/prepare-client-preview-acceptance.js +65 -0
- package/dist/scripts/preview-page-server-smoke.js +63 -0
- package/dist/scripts/run-ts-test.js +41 -0
- package/dist/scripts/verify-agent-contract.js +410 -0
- package/dist/scripts/vibe-ppe-test-preload.cjs +23 -0
- package/dist/scripts/vibe-upload-demo.js +72 -0
- package/dist/scripts/vibe-upload-smoke.js +412 -0
- package/dist/scripts/wasm-smoke.js +53 -0
- package/dist/scripts/wasm-worker.js +63 -0
- package/dist/scripts/worker.js +28 -22
- package/package.json +36 -4
- package/CHANGELOG.md +0 -277
- package/dist/public/assets/Detail-rVxbEDgh.js.br +0 -0
- package/dist/public/assets/baseForm-kBTxn8h8.js.br +0 -0
- package/dist/public/assets/index-Bjn0ctEd.js.br +0 -0
- package/dist/public/assets/index-BxokHznc.js.br +0 -0
- package/dist/public/assets/index-Cl0_vMWE.js.br +0 -0
- package/dist/public/assets/index-DHdJsD10.js.br +0 -0
- package/dist/public/assets/index-DIcz5ciL.js.br +0 -0
- package/dist/public/assets/index-O_L1tU2b.js.br +0 -0
- package/dist/public/assets/index-aSl6Wdrp.js.br +0 -0
- package/dist/public/assets/index-odEI7YQF.js.br +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
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 { spawn } = require('node:child_process');
|
|
6
|
+
const WebSocket = require('ws');
|
|
7
|
+
|
|
8
|
+
const delay = milliseconds =>
|
|
9
|
+
new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
10
|
+
|
|
11
|
+
async function waitForFile(file, process, timeoutMs = 20000) {
|
|
12
|
+
const deadline = Date.now() + timeoutMs;
|
|
13
|
+
while (Date.now() < deadline) {
|
|
14
|
+
if (fs.existsSync(file)) return;
|
|
15
|
+
if (process.exitCode !== null) {
|
|
16
|
+
throw new Error(`ttmg dev exited before writing ${file}`);
|
|
17
|
+
}
|
|
18
|
+
await delay(100);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Timed out waiting for ${file}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function sendGameReady(port) {
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
const socket = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
26
|
+
socket.once('error', reject);
|
|
27
|
+
socket.once('open', () => {
|
|
28
|
+
socket.send(
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
from: 'browser',
|
|
31
|
+
method: 'runtimeEvent',
|
|
32
|
+
payload: {
|
|
33
|
+
method: 'reportScene',
|
|
34
|
+
payload: { sceneId: 9999 },
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
error => {
|
|
38
|
+
if (error) {
|
|
39
|
+
reject(error);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
socket.close();
|
|
43
|
+
resolve();
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function run() {
|
|
51
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-game-ready-'));
|
|
52
|
+
const gameDirectory = path.join(root, 'game');
|
|
53
|
+
const evidenceDirectory = path.join(root, 'evidence');
|
|
54
|
+
fs.mkdirSync(gameDirectory);
|
|
55
|
+
fs.mkdirSync(evidenceDirectory);
|
|
56
|
+
fs.writeFileSync(path.join(gameDirectory, 'game.js'), 'console.log("game");\n');
|
|
57
|
+
fs.writeFileSync(path.join(gameDirectory, 'game.json'), '{}\n');
|
|
58
|
+
|
|
59
|
+
let stdout = '';
|
|
60
|
+
let stderr = '';
|
|
61
|
+
const child = spawn(
|
|
62
|
+
process.execPath,
|
|
63
|
+
[
|
|
64
|
+
path.resolve(__dirname, '../dist/index.js'),
|
|
65
|
+
'dev',
|
|
66
|
+
'--client-key',
|
|
67
|
+
'smoke-client',
|
|
68
|
+
'--events',
|
|
69
|
+
'ndjson',
|
|
70
|
+
'--no-open',
|
|
71
|
+
'--evidence-dir',
|
|
72
|
+
evidenceDirectory,
|
|
73
|
+
'--timeout',
|
|
74
|
+
'20',
|
|
75
|
+
],
|
|
76
|
+
{
|
|
77
|
+
cwd: gameDirectory,
|
|
78
|
+
env: { ...process.env, TTMG_DEV_NO_OPEN: '1' },
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
child.stdout.on('data', chunk => {
|
|
83
|
+
stdout += chunk;
|
|
84
|
+
});
|
|
85
|
+
child.stderr.on('data', chunk => {
|
|
86
|
+
stderr += chunk;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const manifestFile = path.join(evidenceDirectory, 'manifest.json');
|
|
91
|
+
await waitForFile(manifestFile, child);
|
|
92
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
93
|
+
await sendGameReady(manifest.network.wsPort);
|
|
94
|
+
await delay(300);
|
|
95
|
+
child.kill('SIGTERM');
|
|
96
|
+
const exitCode = await new Promise(resolve => child.once('close', resolve));
|
|
97
|
+
assert.equal(exitCode, 0, stderr || stdout);
|
|
98
|
+
|
|
99
|
+
const stdoutEvents = stdout
|
|
100
|
+
.trim()
|
|
101
|
+
.split(/\r?\n/)
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map(line => JSON.parse(line));
|
|
104
|
+
assert.ok(stdoutEvents.length > 0, 'dev did not emit NDJSON events');
|
|
105
|
+
assert.equal(
|
|
106
|
+
stdoutEvents.every(
|
|
107
|
+
event =>
|
|
108
|
+
event.schemaVersion === '1' &&
|
|
109
|
+
typeof event.sessionId === 'string' &&
|
|
110
|
+
Number.isInteger(event.seq) &&
|
|
111
|
+
typeof event.type === 'string',
|
|
112
|
+
),
|
|
113
|
+
true,
|
|
114
|
+
'dev stdout contained a non-event JSON object',
|
|
115
|
+
);
|
|
116
|
+
assert.equal(
|
|
117
|
+
stdoutEvents.some(
|
|
118
|
+
event =>
|
|
119
|
+
event.type === 'lifecycle' &&
|
|
120
|
+
event.payload?.state === 'GAME_READY',
|
|
121
|
+
),
|
|
122
|
+
true,
|
|
123
|
+
'dev stdout did not contain the GAME_READY event',
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const events = fs
|
|
127
|
+
.readFileSync(path.join(evidenceDirectory, 'events.ndjson'), 'utf8')
|
|
128
|
+
.trim()
|
|
129
|
+
.split('\n')
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map(JSON.parse);
|
|
132
|
+
const ready = events.find(
|
|
133
|
+
event =>
|
|
134
|
+
event.type === 'lifecycle' && event.payload?.state === 'GAME_READY',
|
|
135
|
+
);
|
|
136
|
+
assert.equal(ready?.source, 'runtime');
|
|
137
|
+
assert.equal(ready?.payload?.sceneId, 9999);
|
|
138
|
+
|
|
139
|
+
const summary = JSON.parse(
|
|
140
|
+
fs.readFileSync(path.join(evidenceDirectory, 'summary.json'), 'utf8'),
|
|
141
|
+
);
|
|
142
|
+
assert.equal(summary.status, 'passed');
|
|
143
|
+
assert.equal(summary.errorCode, null);
|
|
144
|
+
} finally {
|
|
145
|
+
if (child.exitCode === null) child.kill('SIGTERM');
|
|
146
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log('dev GAME_READY process smoke passed');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
run().catch(error => {
|
|
153
|
+
console.error(error);
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
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 { spawn } = require('node:child_process');
|
|
6
|
+
|
|
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-workflow-process-'));
|
|
9
|
+
const game = path.join(root, 'game');
|
|
10
|
+
fs.mkdirSync(game);
|
|
11
|
+
fs.writeFileSync(path.join(game, 'game.js'), 'console.log("round A");');
|
|
12
|
+
fs.writeFileSync(path.join(game, 'game.json'), '{}');
|
|
13
|
+
// Restarted projects contain previous evidence/builds; startup must exclude it.
|
|
14
|
+
fs.mkdirSync(path.join(game, '.ttmg', 'previous-build'), { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(game, '.ttmg', 'previous-build', 'game.js'), 'throw new Error("old output must not be compiled");');
|
|
16
|
+
const configPath = path.join(root, 'ttmgrc.json');
|
|
17
|
+
const initialConfig = JSON.stringify({ clientKey: 'unchanged', lang: 'en-US' });
|
|
18
|
+
fs.writeFileSync(configPath, initialConfig);
|
|
19
|
+
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
20
|
+
function run(args, directory = game) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(process.execPath, [entry, ...args], {
|
|
23
|
+
cwd: directory,
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
NO_COLOR: '1',
|
|
27
|
+
FORCE_COLOR: '0',
|
|
28
|
+
TTMG_DEV_NO_OPEN: '1',
|
|
29
|
+
TTMGRC_PATH: configPath,
|
|
30
|
+
},
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
});
|
|
33
|
+
let stdout = '',
|
|
34
|
+
stderr = '';
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
child.kill('SIGKILL');
|
|
37
|
+
reject(new Error('CLI timeout'));
|
|
38
|
+
}, 75000);
|
|
39
|
+
child.stdout.on('data', data => {
|
|
40
|
+
stdout += data;
|
|
41
|
+
});
|
|
42
|
+
child.stderr.on('data', data => {
|
|
43
|
+
stderr += data;
|
|
44
|
+
});
|
|
45
|
+
child.once('error', reject);
|
|
46
|
+
child.once('close', code => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
try {
|
|
49
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
50
|
+
assert.equal(
|
|
51
|
+
lines.length,
|
|
52
|
+
1,
|
|
53
|
+
'structured stdout must contain one object: ' + stderr,
|
|
54
|
+
);
|
|
55
|
+
resolve({ code, result: JSON.parse(lines[0]) });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
reject(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
let owner;
|
|
63
|
+
let secondOwner;
|
|
64
|
+
async function main() {
|
|
65
|
+
try {
|
|
66
|
+
const start = [
|
|
67
|
+
'dev',
|
|
68
|
+
'--mode',
|
|
69
|
+
'client-preview',
|
|
70
|
+
'--update-mode',
|
|
71
|
+
'manual',
|
|
72
|
+
'--background',
|
|
73
|
+
'--client-key',
|
|
74
|
+
'smoke-client',
|
|
75
|
+
'--record-logs',
|
|
76
|
+
'--timeout',
|
|
77
|
+
'90',
|
|
78
|
+
'--format',
|
|
79
|
+
'json',
|
|
80
|
+
];
|
|
81
|
+
const first = await run(start);
|
|
82
|
+
assert.equal(first.code, 0, JSON.stringify(first.result));
|
|
83
|
+
owner = first.result;
|
|
84
|
+
assert.equal(owner.updateMode, 'manual');
|
|
85
|
+
assert.equal(owner.reused, false);
|
|
86
|
+
assert.equal(fs.existsSync(owner.launch.qrPngPath), true);
|
|
87
|
+
assert.ok(owner.evidence.runtimeLogs);
|
|
88
|
+
assert.equal(fs.existsSync(path.join(owner.evidence.directory, 'build-output', '.ttmg')), false);
|
|
89
|
+
assert.equal(fs.existsSync(path.join(owner.evidence.directory, 'build-output', '__GAME__', '.ttmg')), false);
|
|
90
|
+
assert.ok(
|
|
91
|
+
fs.existsSync(path.join(owner.evidence.directory, 'build-output')),
|
|
92
|
+
);
|
|
93
|
+
const reused = await run(start);
|
|
94
|
+
assert.equal(reused.result.sessionId, owner.sessionId);
|
|
95
|
+
assert.equal(reused.result.reused, true);
|
|
96
|
+
const otherGame = path.join(root, 'other-game');
|
|
97
|
+
fs.mkdirSync(otherGame);
|
|
98
|
+
fs.writeFileSync(path.join(otherGame, 'game.js'), 'console.log("independent game");');
|
|
99
|
+
fs.writeFileSync(path.join(otherGame, 'game.json'), '{}');
|
|
100
|
+
const second = await run(start, otherGame);
|
|
101
|
+
assert.equal(second.code, 0, JSON.stringify(second.result));
|
|
102
|
+
secondOwner = second.result;
|
|
103
|
+
assert.notEqual(secondOwner.evidence.directory, owner.evidence.directory);
|
|
104
|
+
assert.ok(fs.existsSync(path.join(secondOwner.evidence.directory, 'build-output')));
|
|
105
|
+
fs.writeFileSync(configPath, JSON.stringify({ clientKey: 'different-global-key', lang: 'en-US' }));
|
|
106
|
+
for (const preview of [owner, secondOwner]) {
|
|
107
|
+
const response = await fetch(new URL('/game/config', preview.endpoints.page));
|
|
108
|
+
const config = await response.json();
|
|
109
|
+
assert.equal(config.data.clientKey, 'smoke-client');
|
|
110
|
+
}
|
|
111
|
+
fs.writeFileSync(configPath, initialConfig);
|
|
112
|
+
// Saving files alone must not start an intermediate build in manual mode.
|
|
113
|
+
const eventCount = fs
|
|
114
|
+
.readFileSync(owner.evidence.events, 'utf8')
|
|
115
|
+
.split('\n')
|
|
116
|
+
.filter(Boolean).length;
|
|
117
|
+
fs.writeFileSync(path.join(game, 'game.js'), 'console.log("round B");');
|
|
118
|
+
await pause(3500);
|
|
119
|
+
assert.equal(
|
|
120
|
+
fs.readFileSync(owner.evidence.events, 'utf8').split('\n').filter(Boolean)
|
|
121
|
+
.length,
|
|
122
|
+
eventCount,
|
|
123
|
+
);
|
|
124
|
+
const sync = await run([
|
|
125
|
+
'dev',
|
|
126
|
+
'sync',
|
|
127
|
+
'--request-id',
|
|
128
|
+
'round-1',
|
|
129
|
+
'--format',
|
|
130
|
+
'json',
|
|
131
|
+
]);
|
|
132
|
+
assert.equal(sync.code, 0, JSON.stringify(sync.result));
|
|
133
|
+
assert.equal(sync.result.iterationId, 'round-1');
|
|
134
|
+
const observed = await run([
|
|
135
|
+
'dev',
|
|
136
|
+
'observe',
|
|
137
|
+
'--iteration',
|
|
138
|
+
'round-1',
|
|
139
|
+
'--timeout',
|
|
140
|
+
'30',
|
|
141
|
+
'--format',
|
|
142
|
+
'json',
|
|
143
|
+
]);
|
|
144
|
+
assert.equal(
|
|
145
|
+
observed.result.status,
|
|
146
|
+
'needs_action',
|
|
147
|
+
JSON.stringify(observed.result),
|
|
148
|
+
);
|
|
149
|
+
assert.equal(observed.result.nextAction, 'connect_device');
|
|
150
|
+
assert.equal(observed.result.runtimeBuildVerified, false);
|
|
151
|
+
assert.equal(observed.result.launch.qrPngPath, owner.launch.qrPngPath);
|
|
152
|
+
assert.ok(observed.result.sourceDigest);
|
|
153
|
+
fs.writeFileSync(path.join(game, 'game.js'), 'console.log("round C");');
|
|
154
|
+
const retry = await run([
|
|
155
|
+
'dev',
|
|
156
|
+
'sync',
|
|
157
|
+
'--request-id',
|
|
158
|
+
'round-1',
|
|
159
|
+
'--format',
|
|
160
|
+
'json',
|
|
161
|
+
]);
|
|
162
|
+
assert.equal(retry.result.buildId, sync.result.buildId);
|
|
163
|
+
const next = await run([
|
|
164
|
+
'dev',
|
|
165
|
+
'sync',
|
|
166
|
+
'--request-id',
|
|
167
|
+
'round-2',
|
|
168
|
+
'--format',
|
|
169
|
+
'json',
|
|
170
|
+
]);
|
|
171
|
+
const nextResult = await run([
|
|
172
|
+
'dev',
|
|
173
|
+
'observe',
|
|
174
|
+
'--iteration',
|
|
175
|
+
'round-2',
|
|
176
|
+
'--timeout',
|
|
177
|
+
'30',
|
|
178
|
+
'--format',
|
|
179
|
+
'json',
|
|
180
|
+
]);
|
|
181
|
+
assert.equal(
|
|
182
|
+
nextResult.result.status,
|
|
183
|
+
'needs_action',
|
|
184
|
+
JSON.stringify(nextResult.result),
|
|
185
|
+
);
|
|
186
|
+
assert.notEqual(next.result.buildId, sync.result.buildId);
|
|
187
|
+
assert.notEqual(
|
|
188
|
+
nextResult.result.sourceDigest,
|
|
189
|
+
observed.result.sourceDigest,
|
|
190
|
+
);
|
|
191
|
+
const stale = await run([
|
|
192
|
+
'dev',
|
|
193
|
+
'observe',
|
|
194
|
+
'--iteration',
|
|
195
|
+
'round-1',
|
|
196
|
+
'--format',
|
|
197
|
+
'json',
|
|
198
|
+
]);
|
|
199
|
+
assert.equal(stale.result.errorCode, 'DEV_ITERATION_SUPERSEDED');
|
|
200
|
+
assert.equal(stale.code, 1);
|
|
201
|
+
fs.writeFileSync(path.join(game, 'game.js'), 'function {');
|
|
202
|
+
await run(['dev', 'sync', '--request-id', 'broken', '--format', 'json']);
|
|
203
|
+
const failed = await run([
|
|
204
|
+
'dev',
|
|
205
|
+
'observe',
|
|
206
|
+
'--iteration',
|
|
207
|
+
'broken',
|
|
208
|
+
'--timeout',
|
|
209
|
+
'30',
|
|
210
|
+
'--format',
|
|
211
|
+
'json',
|
|
212
|
+
]);
|
|
213
|
+
assert.equal(failed.result.status, 'failed', JSON.stringify(failed.result));
|
|
214
|
+
fs.writeFileSync(path.join(game, 'game.js'), 'console.log("recovered");');
|
|
215
|
+
await run(['dev', 'sync', '--request-id', 'recovered', '--format', 'json']);
|
|
216
|
+
const recovery = await run([
|
|
217
|
+
'dev',
|
|
218
|
+
'observe',
|
|
219
|
+
'--iteration',
|
|
220
|
+
'recovered',
|
|
221
|
+
'--timeout',
|
|
222
|
+
'30',
|
|
223
|
+
'--format',
|
|
224
|
+
'json',
|
|
225
|
+
]);
|
|
226
|
+
assert.equal(
|
|
227
|
+
recovery.result.status,
|
|
228
|
+
'needs_action',
|
|
229
|
+
JSON.stringify(recovery.result),
|
|
230
|
+
);
|
|
231
|
+
assert.equal(
|
|
232
|
+
fs.existsSync(path.join(game, '.ttmg', 'preview-start.lock')),
|
|
233
|
+
false,
|
|
234
|
+
);
|
|
235
|
+
assert.equal(fs.readFileSync(configPath, 'utf8'), initialConfig);
|
|
236
|
+
console.log(
|
|
237
|
+
'manual preview process smoke passed: start/reuse, two rounds, stale rejection, failure/recovery; no device connected',
|
|
238
|
+
);
|
|
239
|
+
} finally {
|
|
240
|
+
for (const preview of [owner, secondOwner].filter(Boolean)) {
|
|
241
|
+
const stopped = await run([
|
|
242
|
+
'dev',
|
|
243
|
+
'session',
|
|
244
|
+
'stop',
|
|
245
|
+
preview.sessionId,
|
|
246
|
+
'--sessions-dir',
|
|
247
|
+
preview.evidence.directory,
|
|
248
|
+
'--format',
|
|
249
|
+
'json',
|
|
250
|
+
]);
|
|
251
|
+
assert.equal(stopped.code, 0, JSON.stringify(stopped.result));
|
|
252
|
+
assert.equal(fs.existsSync(preview.evidence.summary), true);
|
|
253
|
+
}
|
|
254
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
main().catch(error => {
|
|
258
|
+
console.error(error);
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function prepareFixture(output) {
|
|
6
|
+
if (!output) throw new Error('Provide a new output directory.');
|
|
7
|
+
const root = path.resolve(output);
|
|
8
|
+
fs.mkdirSync(root, { recursive: false });
|
|
9
|
+
const runId = crypto.randomUUID();
|
|
10
|
+
try {
|
|
11
|
+
for (const round of ['A', 'B']) {
|
|
12
|
+
const directory = path.join(root, `round-${round.toLowerCase()}`);
|
|
13
|
+
fs.mkdirSync(directory);
|
|
14
|
+
const marker = `TTMG_ACCEPTANCE:${runId}:${round}`;
|
|
15
|
+
const code = `// Generated acceptance fixture. This is not a commercial game.
|
|
16
|
+
const marker = ${JSON.stringify(marker)};
|
|
17
|
+
const canvas = tt.createCanvas();
|
|
18
|
+
const info = tt.getSystemInfoSync();
|
|
19
|
+
canvas.width = info.windowWidth;
|
|
20
|
+
canvas.height = info.windowHeight;
|
|
21
|
+
const context = canvas.getContext('2d');
|
|
22
|
+
function render() {
|
|
23
|
+
context.fillStyle = ${JSON.stringify(round === 'A' ? '#175cd3' : '#067647')};
|
|
24
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
25
|
+
context.fillStyle = '#ffffff';
|
|
26
|
+
context.font = 'bold 36px sans-serif';
|
|
27
|
+
context.fillText('ROUND ${round}', 20, 70);
|
|
28
|
+
context.font = '16px monospace';
|
|
29
|
+
context.fillText(${JSON.stringify(runId.slice(0, 18))}, 20, 110);
|
|
30
|
+
context.fillText(${JSON.stringify(runId.slice(18))}, 20, 138);
|
|
31
|
+
console.info(marker + ':info');
|
|
32
|
+
console.warn(marker + ':warn');
|
|
33
|
+
console.error(marker + ':error:EXPECTED_FIXTURE_LOG');
|
|
34
|
+
}
|
|
35
|
+
render();
|
|
36
|
+
tt.onShow(render);
|
|
37
|
+
`;
|
|
38
|
+
fs.writeFileSync(path.join(directory, 'game.js'), code);
|
|
39
|
+
fs.writeFileSync(path.join(directory, 'game.json'), '{}\n');
|
|
40
|
+
}
|
|
41
|
+
fs.cpSync(path.join(root, 'round-a'), path.join(root, 'project'), { recursive: true });
|
|
42
|
+
const result = {
|
|
43
|
+
schemaVersion: '1', status: 'prepared', runId, project: path.join(root, 'project'),
|
|
44
|
+
rounds: ['A', 'B'].map(round => ({ round, source: path.join(root, `round-${round.toLowerCase()}`),
|
|
45
|
+
marker: `TTMG_ACCEPTANCE:${runId}:${round}`, severities: ['info', 'warn', 'error'],
|
|
46
|
+
expectedScreen: `ROUND ${round} and the complete runId` })),
|
|
47
|
+
clientStarted: false, deviceVerified: false, commercialAcceptance: 'not_evaluated',
|
|
48
|
+
};
|
|
49
|
+
fs.writeFileSync(path.join(root, 'acceptance.json'), JSON.stringify(result, null, 2) + '\n');
|
|
50
|
+
return result;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
module.exports = { prepareFixture };
|
|
57
|
+
if (require.main === module) {
|
|
58
|
+
try {
|
|
59
|
+
if (process.argv.length !== 3) throw new Error('Usage: node prepare-client-preview-acceptance.js <new-directory>');
|
|
60
|
+
console.log(JSON.stringify(prepareFixture(process.argv[2])));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.log(JSON.stringify({ status: 'failed', error: error.message, deviceVerified: false }));
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const entryPath = path.resolve(__dirname, '../dist/index.js');
|
|
6
|
+
const child = spawn(process.execPath, [entryPath, '__preview-page-server'], {
|
|
7
|
+
detached: true,
|
|
8
|
+
windowsHide: true,
|
|
9
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const ready = new Promise((resolve, reject) => {
|
|
13
|
+
const timeout = setTimeout(
|
|
14
|
+
() => reject(new Error('preview page worker did not become ready')),
|
|
15
|
+
5000,
|
|
16
|
+
);
|
|
17
|
+
child.once('error', reject);
|
|
18
|
+
child.once('exit', code =>
|
|
19
|
+
reject(new Error(`preview page worker exited early (${code})`)),
|
|
20
|
+
);
|
|
21
|
+
child.on('message', message => {
|
|
22
|
+
if (message?.type === 'error') reject(new Error(message.error));
|
|
23
|
+
if (message?.type === 'ready') {
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
resolve(message);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
child.send({
|
|
32
|
+
type: 'start',
|
|
33
|
+
authorizationUrl: 'https://www.tiktok.com/ttmg/dev/client-key',
|
|
34
|
+
previewUrl: 'https://www.tiktok.com/preview/process-smoke',
|
|
35
|
+
clientKey: 'client-key',
|
|
36
|
+
projectName: 'Process Smoke Game',
|
|
37
|
+
versionId: 'asset-smoke',
|
|
38
|
+
token: 'b'.repeat(48),
|
|
39
|
+
ttlMs: 5000,
|
|
40
|
+
});
|
|
41
|
+
const result = await ready;
|
|
42
|
+
assert.match(result.url, /^http:\/\/127\.0\.0\.1:\d+\/preview\/b{48}$/);
|
|
43
|
+
const response = await fetch(result.url);
|
|
44
|
+
assert.equal(response.status, 200);
|
|
45
|
+
assert.equal(
|
|
46
|
+
response.headers.get('content-type'),
|
|
47
|
+
'text/html; charset=utf-8',
|
|
48
|
+
);
|
|
49
|
+
const html = await response.text();
|
|
50
|
+
assert.match(html, /Process Smoke Game/);
|
|
51
|
+
assert.equal((html.match(/data:image\/png;base64,/g) || []).length, 2);
|
|
52
|
+
console.log('preview page server process smoke passed');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main()
|
|
56
|
+
.catch(error => {
|
|
57
|
+
console.error(error);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
if (child.connected) child.disconnect();
|
|
62
|
+
child.kill();
|
|
63
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const esbuild = require('esbuild');
|
|
6
|
+
|
|
7
|
+
const entry = process.argv[2];
|
|
8
|
+
if (!entry) {
|
|
9
|
+
throw new Error('Missing TypeScript test entry.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-cli-test-'));
|
|
13
|
+
const outputFile = path.join(tempDir, 'test.cjs');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
esbuild.buildSync({
|
|
17
|
+
entryPoints: [path.resolve(process.cwd(), entry)],
|
|
18
|
+
bundle: true,
|
|
19
|
+
platform: 'node',
|
|
20
|
+
format: 'cjs',
|
|
21
|
+
target: 'node20',
|
|
22
|
+
outfile: outputFile,
|
|
23
|
+
external: ['esbuild'],
|
|
24
|
+
sourcemap: 'inline',
|
|
25
|
+
logLevel: 'warning',
|
|
26
|
+
});
|
|
27
|
+
execFileSync(process.execPath, [outputFile], {
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
NODE_PATH: [
|
|
32
|
+
path.resolve(process.cwd(), 'node_modules'),
|
|
33
|
+
process.env.NODE_PATH,
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(path.delimiter),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
} finally {
|
|
40
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
41
|
+
}
|