ravensight-playtest 0.2.9 → 0.3.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 +105 -12
- package/docs/ADAPTERS.md +2 -0
- package/docs/NATIVE_RUNNER.md +243 -0
- package/package.json +2 -1
- package/src/cli.js +6 -0
- package/src/commands/check.js +330 -106
- package/src/commands/run.js +73 -6
- package/src/run/aggregate.js +103 -36
- package/src/run/args.js +2 -0
- package/src/run/drivers/driver.js +7 -0
- package/src/run/drivers/web.js +619 -43
- package/src/run/drivers/webdriver-probe.js +353 -0
- package/src/run/dropped.js +7 -7
- package/src/run/personaLoop.js +603 -45
- package/src/run/regenerate.js +24 -4
- package/src/run/resume.js +7 -1
- package/src/run/tools.js +49 -12
- package/src/run/usage.js +19 -1
package/src/commands/check.js
CHANGED
|
@@ -10,6 +10,10 @@ import { inspectGodot } from '../godot.js';
|
|
|
10
10
|
import { CLI_VERSION, compareVersions } from '../version.js';
|
|
11
11
|
import { ui } from '../ui/index.js';
|
|
12
12
|
import { ExitCode } from '../errors.js';
|
|
13
|
+
import { completenessFor } from './brief.js';
|
|
14
|
+
import { WEBDRIVER_NAMES } from '../run/drivers/webdriver.js';
|
|
15
|
+
import { inspectNativeBuild, probeAutomation, platformWords } from '../run/drivers/webdriver-probe.js';
|
|
16
|
+
import path from 'node:path';
|
|
13
17
|
|
|
14
18
|
export const NODE_MIN_MAJOR = 22;
|
|
15
19
|
const DISK_WARN_BYTES = 2 * 1024 * 1024 * 1024;
|
|
@@ -88,6 +92,96 @@ export async function probeBrowser(importer) {
|
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
/** Said by `check` every time, in both outputs, because a customer asked. */
|
|
96
|
+
export const NO_CHARGE_NOTE = 'check reserves no units and starts no job: every request it makes is a free read, and the automation probe talks only to your own local server.';
|
|
97
|
+
|
|
98
|
+
/** `Jump Bunny (g1)`, or just the id when the game has no name. */
|
|
99
|
+
const gameLabel = game => (game.name ? `${game.name} (${game.gameId})` : game.gameId);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The driver this directory would run with, and the target it would point at,
|
|
103
|
+
* resolved the way `run` resolves them. The flag wins, then the repo config.
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} flags
|
|
106
|
+
* @param {Object|null} config
|
|
107
|
+
* @returns {{driverName: string, native: boolean, target: string|null}}
|
|
108
|
+
*/
|
|
109
|
+
export function plannedRun(flags = {}, config = null) {
|
|
110
|
+
const driverName = flags.driver || (config && config.driver) || 'playwright_web';
|
|
111
|
+
const build = (config && config.build) || {};
|
|
112
|
+
let target = null;
|
|
113
|
+
if (WEBDRIVER_NAMES.includes(driverName) || driverName === 'cli_stdio') {
|
|
114
|
+
target = flags.driverConfig || (build.kind === 'driver_config' ? build.ref : null) || null;
|
|
115
|
+
} else if (driverName === 'playwright_web') {
|
|
116
|
+
target = flags.buildUrl || (build.kind === 'url' ? build.ref : null) || null;
|
|
117
|
+
} else if (driverName === 'godot_driver') {
|
|
118
|
+
target = flags.godotProject || (build.kind === 'godot_project' ? build.ref : null) || null;
|
|
119
|
+
}
|
|
120
|
+
return { driverName, native: WEBDRIVER_NAMES.includes(driverName), target };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The automation rows: the native build, then the automation server. These
|
|
125
|
+
* are the rows `run` repeats as its preflight before it registers a native job,
|
|
126
|
+
* so they take nothing from the account and need no token.
|
|
127
|
+
*
|
|
128
|
+
* @param {{driverName: string, target: string|null, repoRoot?: string}} plan
|
|
129
|
+
* @param {{fetch?: typeof fetch, probeAutomation?: Function, timeouts?: Object}} [deps]
|
|
130
|
+
* @returns {Promise<{rows: Array, automation: Object, build: Object}>}
|
|
131
|
+
*/
|
|
132
|
+
export async function automationChecks(plan, deps = {}) {
|
|
133
|
+
const repoRoot = plan.repoRoot || process.cwd();
|
|
134
|
+
const targetPath = plan.target ? path.resolve(repoRoot, plan.target) : null;
|
|
135
|
+
const words = platformWords(plan.driverName);
|
|
136
|
+
const onRow = deps.onRow || (() => {});
|
|
137
|
+
const rows = [];
|
|
138
|
+
const add = entry => {
|
|
139
|
+
rows.push(entry);
|
|
140
|
+
onRow(entry);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const build = await inspectNativeBuild({ target: targetPath, repoRoot, driverName: plan.driverName });
|
|
144
|
+
add(row('build', build.status, build.detail, build.fix));
|
|
145
|
+
|
|
146
|
+
const probe = await (deps.probeAutomation || probeAutomation)({
|
|
147
|
+
driverName: plan.driverName,
|
|
148
|
+
target: targetPath,
|
|
149
|
+
fetch: deps.fetch,
|
|
150
|
+
timeouts: deps.timeouts,
|
|
151
|
+
onProgress: deps.onProgress
|
|
152
|
+
});
|
|
153
|
+
const fixes = {
|
|
154
|
+
config: 'write a driver config with a loopback endpoint and W3C capabilities; see docs/NATIVE_RUNNER.md',
|
|
155
|
+
reachable: 'start your local W3C automation server (see docs/NATIVE_RUNNER.md) and confirm the endpoint in the driver config',
|
|
156
|
+
session: words.session,
|
|
157
|
+
session_timeout: 'restart the automation server, or wait for it to finish, then run check again before starting a run',
|
|
158
|
+
app: words.app
|
|
159
|
+
};
|
|
160
|
+
if (probe.failed) {
|
|
161
|
+
add(row('automation server', 'fail', probe.detail, fixes[probe.failed]));
|
|
162
|
+
} else if (probe.closed === false) {
|
|
163
|
+
add(row(
|
|
164
|
+
'automation server',
|
|
165
|
+
'warn',
|
|
166
|
+
probe.detail,
|
|
167
|
+
'the probe session could not be closed; restart the automation server if the device stays busy'
|
|
168
|
+
));
|
|
169
|
+
} else {
|
|
170
|
+
add(row('automation server', 'pass', probe.detail));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
rows,
|
|
175
|
+
build: { ref: build.ref, status: build.status },
|
|
176
|
+
automation: {
|
|
177
|
+
endpoint: probe.endpoint,
|
|
178
|
+
reachable: probe.reachable,
|
|
179
|
+
session: probe.session,
|
|
180
|
+
app: probe.app
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
91
185
|
/**
|
|
92
186
|
* Every environment check, as data.
|
|
93
187
|
*
|
|
@@ -96,30 +190,50 @@ export async function probeBrowser(importer) {
|
|
|
96
190
|
* in process server with no terminal involved.
|
|
97
191
|
*
|
|
98
192
|
* Status meanings are load bearing. `fail` is "this command cannot work", and
|
|
99
|
-
* it is what sets exit code 3. `warn` is "something optional is missing
|
|
100
|
-
* CI must not fail on it: ffmpeg and Godot are
|
|
101
|
-
* that never touches them should not be blocked
|
|
193
|
+
* it is what sets exit code 3. `warn` is "something optional is missing, or
|
|
194
|
+
* could not be checked", and CI must not fail on it: ffmpeg and Godot are
|
|
195
|
+
* genuinely optional, and a run that never touches them should not be blocked
|
|
196
|
+
* because they are absent. `pass` is checked and fine.
|
|
197
|
+
*
|
|
198
|
+
* The setup rows are separate on purpose: CLI version, login, project binding,
|
|
199
|
+
* brief, build and (for a native driver) the automation server. A customer was
|
|
200
|
+
* told login was still needed when the login was fine and the missing piece
|
|
201
|
+
* was the automation server; each row now answers one question.
|
|
202
|
+
*
|
|
203
|
+
* Nothing here reserves units or starts a job. The estimate, whoami, brief and
|
|
204
|
+
* pack requests are reads.
|
|
102
205
|
*
|
|
103
206
|
* @param {Object} [flags]
|
|
104
207
|
* @param {string} [flags.apiUrl]
|
|
105
208
|
* @param {string} [flags.game]
|
|
209
|
+
* @param {string} [flags.driver]
|
|
210
|
+
* @param {string} [flags.driverConfig]
|
|
106
211
|
* @param {string[]} [flags.modules]
|
|
107
212
|
* @param {string[]} [flags.personas]
|
|
108
213
|
* @param {string} [flags.repoRoot]
|
|
109
214
|
* @param {boolean} [flags.web] probe the browser whatever the config says
|
|
110
215
|
* @param {Object} [deps]
|
|
111
|
-
* @returns {Promise<{rows: Array, ok: boolean}>}
|
|
216
|
+
* @returns {Promise<{rows: Array, ok: boolean, cli: Object, login: Object, project: Object, brief: Object, build: Object, automation: Object|null}>}
|
|
112
217
|
*/
|
|
113
218
|
export async function runChecks(flags = {}, deps = {}) {
|
|
114
219
|
const apiUrl = flags.apiUrl || resolveApiUrl();
|
|
220
|
+
const repoRoot = flags.repoRoot || process.cwd();
|
|
115
221
|
const rows = [];
|
|
222
|
+
const facts = {
|
|
223
|
+
cli: { installed: CLI_VERSION, latest: null, min_supported: null },
|
|
224
|
+
login: { present: false, valid: false, games: [] },
|
|
225
|
+
project: { game: null, environment: null, in_scope: null },
|
|
226
|
+
brief: { present: null, complete: null, missing: [] },
|
|
227
|
+
build: { driver: null, ref: null },
|
|
228
|
+
automation: null
|
|
229
|
+
};
|
|
116
230
|
|
|
117
231
|
/* ------------------------------------------------------------- runtime */
|
|
118
232
|
|
|
119
233
|
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
120
234
|
rows.push(row(
|
|
121
235
|
'node',
|
|
122
|
-
nodeMajor >= NODE_MIN_MAJOR ? '
|
|
236
|
+
nodeMajor >= NODE_MIN_MAJOR ? 'pass' : 'fail',
|
|
123
237
|
`v${process.versions.node}`,
|
|
124
238
|
`install Node ${NODE_MIN_MAJOR} or newer`
|
|
125
239
|
));
|
|
@@ -130,7 +244,7 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
130
244
|
let version = null;
|
|
131
245
|
try {
|
|
132
246
|
version = await api.cli.version();
|
|
133
|
-
rows.push(row('api reachable', '
|
|
247
|
+
rows.push(row('api reachable', 'pass', `${apiUrl} (schema ${version.schema_version})`));
|
|
134
248
|
} catch (error) {
|
|
135
249
|
rows.push(row(
|
|
136
250
|
'api reachable',
|
|
@@ -140,15 +254,27 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
140
254
|
));
|
|
141
255
|
}
|
|
142
256
|
|
|
257
|
+
/* --------------------------------------------------------- cli version */
|
|
258
|
+
|
|
143
259
|
if (version) {
|
|
260
|
+
facts.cli.latest = version.latest || null;
|
|
261
|
+
facts.cli.min_supported = version.min_supported || null;
|
|
144
262
|
const tooOld = compareVersions(CLI_VERSION, version.min_supported) < 0;
|
|
145
263
|
const behind = compareVersions(CLI_VERSION, version.latest) < 0;
|
|
264
|
+
const ahead = compareVersions(CLI_VERSION, version.latest) > 0;
|
|
146
265
|
rows.push(row(
|
|
147
266
|
'cli version',
|
|
148
|
-
tooOld ? 'fail' : behind ? 'warn' : '
|
|
149
|
-
|
|
267
|
+
tooOld ? 'fail' : behind ? 'warn' : 'pass',
|
|
268
|
+
`installed ${CLI_VERSION}; server latest ${version.latest}, min_supported ${version.min_supported}${ahead ? ' (installed is newer than the server lists)' : ''}`,
|
|
150
269
|
tooOld || behind ? 'npm install -g ravensight-playtest@latest' : null
|
|
151
270
|
));
|
|
271
|
+
} else {
|
|
272
|
+
rows.push(row(
|
|
273
|
+
'cli version',
|
|
274
|
+
'warn',
|
|
275
|
+
`installed ${CLI_VERSION}; the server's latest and min_supported could not be read`,
|
|
276
|
+
'fix api reachable, then run check again'
|
|
277
|
+
));
|
|
152
278
|
}
|
|
153
279
|
|
|
154
280
|
/* ------------------------------------------------------- the credential */
|
|
@@ -169,138 +295,223 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
169
295
|
};
|
|
170
296
|
rows.push(row(
|
|
171
297
|
'credential store',
|
|
172
|
-
'
|
|
298
|
+
'pass',
|
|
173
299
|
storeDetail(),
|
|
174
300
|
source === 'file' || (source === null && backend === 'file')
|
|
175
301
|
? 'install libsecret for a real keychain on Linux'
|
|
176
302
|
: null
|
|
177
303
|
));
|
|
178
304
|
|
|
305
|
+
/* ---------------------------------------------------------------- login */
|
|
306
|
+
|
|
307
|
+
facts.login.present = Boolean(token);
|
|
308
|
+
let whoami = null;
|
|
309
|
+
let authed = null;
|
|
179
310
|
if (looksLikeIngestKey(token)) {
|
|
180
311
|
rows.push(row(
|
|
181
|
-
'
|
|
312
|
+
'login',
|
|
182
313
|
'fail',
|
|
183
314
|
'that is an ingest key, not a CLI token',
|
|
184
315
|
'run login, or set RAVENSIGHT_TOKEN to a gt_cli_ token'
|
|
185
316
|
));
|
|
186
317
|
} else if (looksLikeMcpToken(token)) {
|
|
187
318
|
rows.push(row(
|
|
188
|
-
'
|
|
319
|
+
'login',
|
|
189
320
|
'fail',
|
|
190
321
|
'This is an MCP personal access token (gt_mcp_). The CLI needs its own token (gt_cli_): run `ravensight-playtest login`, or set RAVENSIGHT_TOKEN to a gt_cli_ token in CI.',
|
|
191
322
|
'ravensight-playtest login'
|
|
192
323
|
));
|
|
193
324
|
} else if (!token) {
|
|
194
|
-
rows.push(row('
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let whoami = null;
|
|
198
|
-
if (token && !looksLikeIngestKey(token) && !looksLikeMcpToken(token)) {
|
|
199
|
-
const authed = deps.authedApi || createClient({ apiUrl, token });
|
|
325
|
+
rows.push(row('login', 'fail', `not logged in to ${host}`, 'ravensight-playtest login'));
|
|
326
|
+
} else {
|
|
327
|
+
authed = deps.authedApi || createClient({ apiUrl, token });
|
|
200
328
|
try {
|
|
201
329
|
whoami = await authed.cli.whoami();
|
|
330
|
+
const games = whoami.games || [];
|
|
331
|
+
facts.login.valid = true;
|
|
332
|
+
facts.login.games = games.map(game => ({ gameId: game.gameId, name: game.name || null }));
|
|
333
|
+
const who = whoami.user.email || whoami.user.name || whoami.user.id;
|
|
202
334
|
rows.push(row(
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
|
|
335
|
+
'login',
|
|
336
|
+
games.length > 0 ? 'pass' : 'warn',
|
|
337
|
+
games.length > 0
|
|
338
|
+
? `${who}, valid, scoped to ${plural(games.length, 'game')}: ${games.map(gameLabel).join(', ')}`
|
|
339
|
+
: `${who}, valid, but scoped to no games`,
|
|
340
|
+
games.length > 0 ? null : 'run ravensight-playtest login again and approve the game for this token'
|
|
206
341
|
));
|
|
207
342
|
} catch (error) {
|
|
208
343
|
const message = error instanceof ApiError ? error.message : String(error && error.message);
|
|
209
|
-
|
|
344
|
+
// Only a 401 or 403 is the server refusing the token. A network error or
|
|
345
|
+
// a 5xx says nothing about the token, so it must not send anyone to log
|
|
346
|
+
// in again over a login that may be fine.
|
|
347
|
+
const refused = error instanceof ApiError && (error.status === 401 || error.status === 403);
|
|
348
|
+
facts.login.valid = refused ? false : null;
|
|
349
|
+
rows.push(refused
|
|
350
|
+
? row('login', 'fail', `a token is stored but the server refused it: ${message}`, 'ravensight-playtest login')
|
|
351
|
+
: row('login', 'warn', `a token is stored, and the server could not verify it right now: ${message}`, 'check api reachable, then run check again'));
|
|
210
352
|
}
|
|
353
|
+
}
|
|
211
354
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const config = flags.config === undefined
|
|
215
|
-
? await loadConfig({ repoRoot: flags.repoRoot })
|
|
216
|
-
: flags.config;
|
|
217
|
-
const gameId = flags.game || (config && config.game_id) || null;
|
|
355
|
+
/* ------------------------------------------------------ project binding */
|
|
218
356
|
|
|
219
|
-
|
|
357
|
+
const config = flags.config === undefined
|
|
358
|
+
? await loadConfig({ repoRoot: flags.repoRoot })
|
|
359
|
+
: flags.config;
|
|
360
|
+
const gameId = flags.game || (config && config.game_id) || null;
|
|
361
|
+
let inScope = null;
|
|
362
|
+
if (!gameId) {
|
|
363
|
+
rows.push(row(
|
|
364
|
+
'project binding',
|
|
365
|
+
'warn',
|
|
366
|
+
'this directory is not linked to a game',
|
|
367
|
+
'ravensight-playtest init --game <gameId>'
|
|
368
|
+
));
|
|
369
|
+
} else {
|
|
370
|
+
const scoped = whoami ? (whoami.games || []).find(game => game.gameId === gameId) : null;
|
|
371
|
+
const environment = (scoped && scoped.environment) || (config && config.environment) || null;
|
|
372
|
+
facts.project.game = gameId;
|
|
373
|
+
facts.project.environment = environment;
|
|
374
|
+
const linked = `${scoped ? gameLabel(scoped) : gameId}${environment ? `, environment ${environment}` : ''}`;
|
|
375
|
+
if (!whoami) {
|
|
220
376
|
rows.push(row(
|
|
221
|
-
'
|
|
377
|
+
'project binding',
|
|
222
378
|
'warn',
|
|
223
|
-
|
|
224
|
-
'
|
|
379
|
+
`linked to ${linked}; whether the token can reach it is unknown until login passes`,
|
|
380
|
+
'fix the login row, then run check again'
|
|
225
381
|
));
|
|
226
|
-
} else
|
|
227
|
-
|
|
382
|
+
} else {
|
|
383
|
+
inScope = Boolean(scoped);
|
|
384
|
+
facts.project.in_scope = inScope;
|
|
228
385
|
rows.push(row(
|
|
229
|
-
'
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
386
|
+
'project binding',
|
|
387
|
+
inScope ? 'pass' : 'fail',
|
|
388
|
+
inScope ? `linked to ${linked}, in the token's scope` : `linked to ${linked}, which is not in this token's scope`,
|
|
389
|
+
inScope ? null : 'run ravensight-playtest login again and approve this game, or relink with init --game <gameId> --force'
|
|
233
390
|
));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* ---------------------------------------------------------------- brief */
|
|
234
395
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// allowed, it may just outrun its wall clock, which is worth seeing
|
|
259
|
-
// here rather than only after `run` has already asked for a price.
|
|
260
|
-
if (estimate.wall_warning) {
|
|
261
|
-
rows.push(row('persona wall clock', 'warn', estimate.wall_warning));
|
|
262
|
-
}
|
|
263
|
-
} catch (error) {
|
|
264
|
-
rows.push(row(
|
|
265
|
-
'balance',
|
|
266
|
-
'warn',
|
|
267
|
-
error instanceof ApiError ? error.message : String(error && error.message)
|
|
268
|
-
));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/* ------------------------------------------------------ pack cache */
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const pack = await packs.get({ api: authed, gameId, modules });
|
|
275
|
-
const manifest = (await authed.packs.manifest()).body;
|
|
276
|
-
const verified = packs.verifyAgainstManifest(manifest, pack.bundle);
|
|
277
|
-
rows.push(row(
|
|
278
|
-
'content pack',
|
|
279
|
-
verified.ok ? 'ok' : 'fail',
|
|
280
|
-
verified.ok
|
|
281
|
-
? `${plural(pack.bundle.personas.length, 'persona')}, ${plural(verified.checked, 'component')} verified${pack.stale ? ' (cached)' : ''}`
|
|
282
|
-
: `these components do not match the manifest: ${verified.mismatched.join(', ')}`,
|
|
283
|
-
verified.ok ? null : `delete ${paths.packCacheDir(apiUrl)} and run check again`
|
|
284
|
-
));
|
|
285
|
-
} catch (error) {
|
|
286
|
-
rows.push(row(
|
|
287
|
-
'content pack',
|
|
288
|
-
'fail',
|
|
289
|
-
error instanceof ApiError ? error.message : String(error && error.message),
|
|
290
|
-
'the CLI cannot run without personas and schemas'
|
|
291
|
-
));
|
|
292
|
-
}
|
|
396
|
+
if (gameId && whoami && inScope) {
|
|
397
|
+
try {
|
|
398
|
+
const answer = await authed.brief.get(gameId);
|
|
399
|
+
const current = (answer && answer.brief) || null;
|
|
400
|
+
if (!current) {
|
|
401
|
+
facts.brief = { present: false, complete: false, missing: [] };
|
|
402
|
+
rows.push(row('brief', 'fail', 'this game has no expectations brief, and a persona run needs one', 'ravensight-playtest brief'));
|
|
403
|
+
} else {
|
|
404
|
+
const completeness = completenessFor(current.fields || {});
|
|
405
|
+
// The server's status is what `run` and `POST /jobs` act on. The local
|
|
406
|
+
// field check only explains a draft, or flags a brief marked complete
|
|
407
|
+
// whose fields a persona needs are empty.
|
|
408
|
+
const complete = current.status === 'complete';
|
|
409
|
+
facts.brief = { present: true, complete, missing: completeness.missing };
|
|
410
|
+
const empty = completeness.missing.length ? `; empty: ${completeness.missing.join(', ')}` : '';
|
|
411
|
+
rows.push(row(
|
|
412
|
+
'brief',
|
|
413
|
+
!complete ? 'fail' : completeness.missing.length ? 'warn' : 'pass',
|
|
414
|
+
complete
|
|
415
|
+
? `version ${current.version}, complete${empty || ' (goals, success criteria, controls, how to start)'}`
|
|
416
|
+
: `version ${current.version}, ${current.status}${empty}`,
|
|
417
|
+
complete && !completeness.missing.length ? null : 'ravensight-playtest brief'
|
|
418
|
+
));
|
|
293
419
|
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
rows.push(row(
|
|
422
|
+
'brief',
|
|
423
|
+
'warn',
|
|
424
|
+
`could not read the brief: ${error instanceof ApiError ? error.message : String(error && error.message)}`,
|
|
425
|
+
'run check again; a run refuses to start without a complete brief'
|
|
426
|
+
));
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
rows.push(row('brief', 'warn', 'not checked: it needs a valid login and a linked game in scope', 'fix the login and project binding rows first'));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* ---------------------------------------------- build and automation */
|
|
433
|
+
|
|
434
|
+
const plan = plannedRun(flags, config);
|
|
435
|
+
facts.build.driver = plan.driverName;
|
|
436
|
+
if (plan.native) {
|
|
437
|
+
const native = await automationChecks({ ...plan, repoRoot }, deps);
|
|
438
|
+
rows.push(...native.rows);
|
|
439
|
+
facts.build.ref = native.build.ref;
|
|
440
|
+
facts.automation = native.automation;
|
|
441
|
+
} else {
|
|
442
|
+
facts.build.ref = plan.target;
|
|
443
|
+
rows.push(row(
|
|
444
|
+
'build',
|
|
445
|
+
plan.target ? 'pass' : 'warn',
|
|
446
|
+
plan.target ? `${plan.driverName}: ${plan.target}` : `${plan.driverName}: no build linked yet`,
|
|
447
|
+
plan.target ? null : 'pass --build-url, --godot-project or --driver-config to run, or set build in .ravensight/config.json'
|
|
448
|
+
));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* -------------------------------------------------- balance and pack */
|
|
452
|
+
|
|
453
|
+
if (gameId && whoami && inScope) {
|
|
454
|
+
const modules = flags.modules || (config && config.modules) || ['game_profile', 'persona_playtest'];
|
|
455
|
+
const personas = flags.personas || (config && config.personas) || [];
|
|
456
|
+
try {
|
|
457
|
+
const estimate = await authed.jobs.estimate(gameId, { modules, personas });
|
|
458
|
+
if (estimate.balance_cents === null || estimate.balance_cents === undefined) {
|
|
459
|
+
rows.push(row(
|
|
460
|
+
'balance',
|
|
461
|
+
'warn',
|
|
462
|
+
`estimate ${ui.money(estimate.estimate_cents)}, balance hidden by your role`,
|
|
463
|
+
'ask an owner or admin to check the balance'
|
|
464
|
+
));
|
|
465
|
+
} else {
|
|
466
|
+
rows.push(row(
|
|
467
|
+
'balance',
|
|
468
|
+
estimate.ok ? 'pass' : 'fail',
|
|
469
|
+
`estimate ${ui.money(estimate.estimate_cents)}, available ${ui.money(estimate.balance_cents)}`,
|
|
470
|
+
estimate.ok ? null : `top up at ${estimate.topup_url}`
|
|
471
|
+
));
|
|
472
|
+
}
|
|
473
|
+
// Not a failure and not hidden by role: a job this size is still
|
|
474
|
+
// allowed, it may just outrun its wall clock, which is worth seeing
|
|
475
|
+
// here rather than only after `run` has already asked for a price.
|
|
476
|
+
if (estimate.wall_warning) {
|
|
477
|
+
rows.push(row('persona wall clock', 'warn', estimate.wall_warning));
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
rows.push(row(
|
|
481
|
+
'balance',
|
|
482
|
+
'warn',
|
|
483
|
+
error instanceof ApiError ? error.message : String(error && error.message)
|
|
484
|
+
));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const pack = await packs.get({ api: authed, gameId, modules });
|
|
489
|
+
const manifest = (await authed.packs.manifest()).body;
|
|
490
|
+
const verified = packs.verifyAgainstManifest(manifest, pack.bundle);
|
|
491
|
+
rows.push(row(
|
|
492
|
+
'content pack',
|
|
493
|
+
verified.ok ? 'pass' : 'fail',
|
|
494
|
+
verified.ok
|
|
495
|
+
? `${plural(pack.bundle.personas.length, 'persona')}, ${plural(verified.checked, 'component')} verified${pack.stale ? ' (cached)' : ''}`
|
|
496
|
+
: `these components do not match the manifest: ${verified.mismatched.join(', ')}`,
|
|
497
|
+
verified.ok ? null : `delete ${paths.packCacheDir(apiUrl)} and run check again`
|
|
498
|
+
));
|
|
499
|
+
} catch (error) {
|
|
500
|
+
rows.push(row(
|
|
501
|
+
'content pack',
|
|
502
|
+
'fail',
|
|
503
|
+
error instanceof ApiError ? error.message : String(error && error.message),
|
|
504
|
+
'the CLI cannot run without personas and schemas'
|
|
505
|
+
));
|
|
294
506
|
}
|
|
295
507
|
}
|
|
296
508
|
|
|
297
509
|
/* --------------------------------------------------------- the machine */
|
|
298
510
|
|
|
299
|
-
const repoRoot = flags.repoRoot || process.cwd();
|
|
300
511
|
const free = await freeBytes(repoRoot);
|
|
301
512
|
rows.push(row(
|
|
302
513
|
'disk',
|
|
303
|
-
free === null ? 'warn' : free >= DISK_WARN_BYTES ? '
|
|
514
|
+
free === null ? 'warn' : free >= DISK_WARN_BYTES ? 'pass' : 'warn',
|
|
304
515
|
free === null ? 'could not read free space' : `${ui.bytes(free)} free in ${repoRoot}`,
|
|
305
516
|
'screenshots and a video take 50 to 300 MB per run'
|
|
306
517
|
));
|
|
@@ -308,14 +519,14 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
308
519
|
const machine = recommendConcurrency(deps.machine);
|
|
309
520
|
rows.push(row(
|
|
310
521
|
'cpu and memory',
|
|
311
|
-
'
|
|
522
|
+
'pass',
|
|
312
523
|
`${machine.cpus} logical cpus, ${machine.freeMemGb} GB free, use concurrency ${machine.recommended}`
|
|
313
524
|
));
|
|
314
525
|
|
|
315
526
|
const ffmpeg = await (deps.detectFfmpeg || detectFfmpeg)();
|
|
316
527
|
rows.push(row(
|
|
317
528
|
'ffmpeg (optional)',
|
|
318
|
-
ffmpeg.path ? '
|
|
529
|
+
ffmpeg.path ? 'pass' : 'warn',
|
|
319
530
|
ffmpeg.path ? `${ffmpeg.path}: ${ffmpeg.version}` : 'not found',
|
|
320
531
|
'needed only to re-encode a session video before upload'
|
|
321
532
|
));
|
|
@@ -325,7 +536,7 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
325
536
|
const probe = await probeBrowser(deps.importer);
|
|
326
537
|
rows.push(row(
|
|
327
538
|
'browser',
|
|
328
|
-
probe.ok ? '
|
|
539
|
+
probe.ok ? 'pass' : browserNeed,
|
|
329
540
|
probe.detail,
|
|
330
541
|
probe.ok ? null : BROWSER_FIX
|
|
331
542
|
));
|
|
@@ -337,7 +548,7 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
337
548
|
);
|
|
338
549
|
rows.push(row(
|
|
339
550
|
'godot (optional)',
|
|
340
|
-
godot.binary.path ? '
|
|
551
|
+
godot.binary.path ? 'pass' : 'warn',
|
|
341
552
|
godot.binary.path
|
|
342
553
|
? `${godot.binary.path}${godot.binary.version ? `: ${godot.binary.version}` : ''}`
|
|
343
554
|
// The detector's own message says WHY: nothing found at all, versus a
|
|
@@ -359,7 +570,7 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
359
570
|
};
|
|
360
571
|
rows.push(row(
|
|
361
572
|
'godot project',
|
|
362
|
-
godot.matches === false || godot.unknown ? 'warn' : '
|
|
573
|
+
godot.matches === false || godot.unknown ? 'warn' : 'pass',
|
|
363
574
|
godot.matches === false
|
|
364
575
|
? `project asks for ${godot.projectVersion}, found ${godot.binary.version}`
|
|
365
576
|
: godot.unknown
|
|
@@ -373,7 +584,7 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
373
584
|
));
|
|
374
585
|
}
|
|
375
586
|
|
|
376
|
-
return { rows, ok: rows.every(entry => entry.status !== 'fail') };
|
|
587
|
+
return { rows, ok: rows.every(entry => entry.status !== 'fail'), ...facts };
|
|
377
588
|
}
|
|
378
589
|
|
|
379
590
|
/**
|
|
@@ -383,10 +594,22 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
383
594
|
* @returns {Promise<number>} exit code, 3 when anything failed
|
|
384
595
|
*/
|
|
385
596
|
export async function check(flags = {}, deps = {}) {
|
|
386
|
-
const
|
|
597
|
+
const progress = flags.json ? {} : { onProgress: line => ui.info(line) };
|
|
598
|
+
const { rows, ok, cli, login, project, brief, build, automation } = await runChecks(flags, { ...progress, ...deps });
|
|
387
599
|
|
|
388
600
|
if (flags.json) {
|
|
389
|
-
ui.json({
|
|
601
|
+
ui.json({
|
|
602
|
+
ok,
|
|
603
|
+
reserves_units: false,
|
|
604
|
+
note: NO_CHARGE_NOTE,
|
|
605
|
+
cli,
|
|
606
|
+
login,
|
|
607
|
+
project,
|
|
608
|
+
brief,
|
|
609
|
+
build,
|
|
610
|
+
automation,
|
|
611
|
+
checks: rows
|
|
612
|
+
});
|
|
390
613
|
return ok ? ExitCode.OK : ExitCode.ENVIRONMENT;
|
|
391
614
|
}
|
|
392
615
|
|
|
@@ -397,9 +620,10 @@ export async function check(flags = {}, deps = {}) {
|
|
|
397
620
|
]);
|
|
398
621
|
ui.blank();
|
|
399
622
|
for (const entry of rows) {
|
|
400
|
-
if (entry.status !== '
|
|
623
|
+
if (entry.status !== 'pass' && entry.fix) ui.info(`${entry.name}: ${entry.fix}`);
|
|
401
624
|
}
|
|
402
625
|
ui.blank();
|
|
626
|
+
ui.info(NO_CHARGE_NOTE);
|
|
403
627
|
if (ok) ui.ok('Ready.');
|
|
404
628
|
else ui.error('Some checks failed. Fix the ones above and run check again.');
|
|
405
629
|
return ok ? ExitCode.OK : ExitCode.ENVIRONMENT;
|