ravensight-playtest 0.2.3 → 0.2.5
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 +89 -6
- package/docs/ADAPTERS.md +77 -0
- package/package.json +2 -1
- package/src/cli.js +12 -6
- package/src/commands/check.js +81 -0
- package/src/commands/fix.js +8 -4
- package/src/commands/init.js +29 -4
- package/src/commands/profile.js +2 -1
- package/src/commands/run.js +81 -26
- package/src/run/actionSelector.js +47 -0
- package/src/run/aggregate.js +10 -5
- package/src/run/args.js +19 -3
- package/src/run/context.js +12 -1
- package/src/run/drivers/driver.js +6 -3
- package/src/run/drivers/index.js +6 -6
- package/src/run/drivers/stdio.js +111 -0
- package/src/run/drivers/web.js +153 -7
- package/src/run/drivers/webdriver.js +67 -0
- package/src/run/fixLoop.js +52 -3
- package/src/run/personaLoop.js +204 -16
- package/src/run/resume.js +1 -0
- package/src/run/tools.js +19 -3
- package/src/states.js +4 -1
- package/src/ui/index.js +28 -0
package/README.md
CHANGED
|
@@ -87,6 +87,18 @@ content pack verified by sha256, disk, recommended concurrency, and the two
|
|
|
87
87
|
optional tools. Exit code 3 when any row fails; a missing optional tool is a
|
|
88
88
|
warning and never a failure, so CI does not break on it.
|
|
89
89
|
|
|
90
|
+
For a web game there is also a `browser` row: `check` imports Playwright,
|
|
91
|
+
launches a headless Chromium and closes it, which is exactly how a web run
|
|
92
|
+
starts. A missing browser fails the check when the linked build is a URL or you
|
|
93
|
+
pass `--web`. When no build is linked yet and the configured driver is the
|
|
94
|
+
default `playwright_web`, it is only a warning, so a Godot repo that never
|
|
95
|
+
changed the default is not failed over a browser it will not use. A repo linked
|
|
96
|
+
to a Godot project or a driver config gets no browser row. The fix is:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm i -D playwright && npx playwright install chromium
|
|
100
|
+
```
|
|
101
|
+
|
|
90
102
|
Past `--personas` in `POST /jobs`, requesting more than 100 personas or the same
|
|
91
103
|
slug twice is refused with 400 `too_many_personas`, and a slug this game does
|
|
92
104
|
not have is 400 `unknown_persona`; `run` checks the count against the same
|
|
@@ -101,6 +113,7 @@ automatically. `check` shows that same warning as a row when it applies.
|
|
|
101
113
|
| `--modules <list>` | Price this module list, comma separated. |
|
|
102
114
|
| `--personas <list>` | Price this persona list. Without it the balance row prices no personas, so the quote is lower than a real job's. |
|
|
103
115
|
| `--godot-path <path>` | Where Godot is, if it is not on the PATH. |
|
|
116
|
+
| `--web` | Check the browser even when this repo is not linked to a web build. |
|
|
104
117
|
|
|
105
118
|
### init
|
|
106
119
|
|
|
@@ -108,9 +121,15 @@ Links this repo to a game and writes `.ravensight/config.json`. Refuses a game
|
|
|
108
121
|
your token cannot see, so the first job does not fail with a 404. Adds
|
|
109
122
|
`.ravensight/jobs/` to an existing `.gitignore`.
|
|
110
123
|
|
|
124
|
+
Without `--game`, one visible game is linked as is. With several, `init` lists
|
|
125
|
+
them numbered and asks `Which game? [1-N]` when it is running in a terminal; in
|
|
126
|
+
CI or a pipe it prints the list and asks you to pass `--game` instead. A token
|
|
127
|
+
that can see no games is told so, with the fix (approve a game for the token at
|
|
128
|
+
login).
|
|
129
|
+
|
|
111
130
|
| Flag | Effect |
|
|
112
131
|
|---|---|
|
|
113
|
-
| `--game <gameId>` | The game to link.
|
|
132
|
+
| `--game <gameId>` | The game to link. Optional with one visible game, or in a terminal. |
|
|
114
133
|
| `--force` | Relink a repo that is already linked. |
|
|
115
134
|
|
|
116
135
|
### brief
|
|
@@ -130,6 +149,30 @@ warning `run` shows. A brief is required before `run`; the server answers 409
|
|
|
130
149
|
| `--game <gameId>` | The game, when this repo is not linked. |
|
|
131
150
|
| `--modules <list>`, `--personas <list>` | Price this module and persona list in the estimate it prints. |
|
|
132
151
|
|
|
152
|
+
#### The `brief --from` file
|
|
153
|
+
|
|
154
|
+
A JSON object of brief fields, or the same object wrapped as
|
|
155
|
+
`{ "fields": { ... } }`, which is how a saved brief carries them. The first four
|
|
156
|
+
are what makes a brief complete; the rest are optional.
|
|
157
|
+
|
|
158
|
+
| Field | Shape | Notes |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `goals` | `[{ "goal": "...", "priority": 1 }]` | At least one. `goal` is one of `find_bugs`, `onboarding`, `balance`, `level_flow`, `narrative`, `accessibility`, `performance`. `priority` is 1 (highest) to 5. |
|
|
161
|
+
| `success_criteria` | string | What success looks like, one criterion per line. The aggregate report scores each line. |
|
|
162
|
+
| `build_notes.controls` | string | How to play: keys, buttons, the verbs a persona needs. |
|
|
163
|
+
| `build_notes.how_to_start` | string | Where a persona begins: the URL, the menu path, the first thing to press. |
|
|
164
|
+
| `design_intent_notes` | array of strings | Deliberate choices a persona must not report as bugs. |
|
|
165
|
+
| `known_issues` | array of strings | Issues you already know about, so they are not reported again. |
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"goals": [{ "goal": "onboarding", "priority": 1 }, { "goal": "find_bugs", "priority": 2 }],
|
|
170
|
+
"success_criteria": "A new player reaches level 2\nNobody gets stuck in the menu",
|
|
171
|
+
"build_notes": { "controls": "Arrow keys to move, space to jump", "how_to_start": "Press Start on the title screen" },
|
|
172
|
+
"design_intent_notes": ["The fog on level 1 is intentional"]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
133
176
|
### profile
|
|
134
177
|
|
|
135
178
|
Builds the game profile and the capability report: what your game is, what can
|
|
@@ -158,7 +201,7 @@ splitting the job in two is what gets everything played.
|
|
|
158
201
|
| `--build-url <url>` | The web build to play. |
|
|
159
202
|
| `--godot-project <dir>` | The Godot project directory. |
|
|
160
203
|
| `--cli <command>` | The command that starts a text build. |
|
|
161
|
-
| `--max-actions <n>` |
|
|
204
|
+
| `--max-actions <n>` | Lower the actions one persona may take. It can only lower the persona's own patience (and the server's per run ceiling), never raise it. |
|
|
162
205
|
| `--concurrency <n>` | Personas at once. Default 2; `check` recommends a number for this machine. |
|
|
163
206
|
| `--upload-video` | Upload `session.webm` as well as the reports. |
|
|
164
207
|
| `--upload-transcript` | Upload `transcript.jsonl` as well as the reports. |
|
|
@@ -166,10 +209,24 @@ splitting the job in two is what gets everything played.
|
|
|
166
209
|
| `--headed`, `--headless` | Show or hide the browser. |
|
|
167
210
|
| `--verbose` | Print each turn. |
|
|
168
211
|
|
|
169
|
-
|
|
170
|
-
silently ignored: `--
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
Three flags are accepted and reported as not applied by this release, rather
|
|
213
|
+
than silently ignored: `--quality` (the model tier is the server's, enforced at
|
|
214
|
+
the proxy), `--offline-upload` (uploads already queue) and `--build-dir` (serve
|
|
215
|
+
the build and pass `--build-url`). There is no spending flag: each run's model
|
|
216
|
+
budget is set by the server from what the job was charged, and `--budget-usd` is
|
|
217
|
+
an unknown option (the refusal says where the budget comes from).
|
|
218
|
+
|
|
219
|
+
Each run hands its playtest identity to the game, so the game's own Ravensight
|
|
220
|
+
events from a playtest are marked synthetic rather than counted as a player. A
|
|
221
|
+
Godot game reads it from its environment. A web game's JavaScript SDK reads it
|
|
222
|
+
from the page: the browser driver sets `window.__RAVENSIGHT_PLAYTEST__` and
|
|
223
|
+
`localStorage['ravensight.playtest']` before the game's scripts run. It is never
|
|
224
|
+
sent as a request header, which would reach your game's own API too.
|
|
225
|
+
|
|
226
|
+
When a run reaches its model budget before the report step, nothing more is
|
|
227
|
+
asked of the model. The persona's notes are written to a partial `report.json`
|
|
228
|
+
(`partial: true`, one unreviewed finding per note with the nearest screenshot as
|
|
229
|
+
evidence) and uploaded, so the job's aggregate still counts them.
|
|
173
230
|
|
|
174
231
|
### fix
|
|
175
232
|
|
|
@@ -465,3 +522,29 @@ import { createClient, packs, uploadRunDir } from 'ravensight-playtest';
|
|
|
465
522
|
## License
|
|
466
523
|
|
|
467
524
|
MIT. Copyright Reality Software Entertainment.
|
|
525
|
+
|
|
526
|
+
## Ravensight Playtester action selection
|
|
527
|
+
|
|
528
|
+
The optional `--controller jev` mode uses the existing persona run. A screenshot
|
|
529
|
+
observer proposes actions, Ravensight selects one on the server, and the local
|
|
530
|
+
driver executes only that choice. Ambiguous controls stop the run for review.
|
|
531
|
+
Reports, screenshots and findings use the normal upload and review flow.
|
|
532
|
+
|
|
533
|
+
```sh
|
|
534
|
+
ravensight-playtest run --driver playwright_web --controller jev --build-url https://your-game.example/ --max-actions 12 --headed
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Sign in, initialize the project and save its expectations brief first. The server
|
|
538
|
+
must advertise this controller in its content pack; otherwise the command stops
|
|
539
|
+
before registering a paid job. No provider key belongs in the CLI. Selection costs
|
|
540
|
+
share the existing persona-run budget and do not create another wallet charge.
|
|
541
|
+
Resume preserves the controller recorded in the job journal.
|
|
542
|
+
|
|
543
|
+
For Jump Bunny, use `https://reality-software-entertainment.github.io/poc-jump-bunny/`
|
|
544
|
+
and the public player label **Ravensight Playtester**. This is a local browser run,
|
|
545
|
+
not a hosted browser service. Human playtesting is still needed for subjective
|
|
546
|
+
feedback and timing-sensitive gameplay.
|
|
547
|
+
|
|
548
|
+
## Build adapters
|
|
549
|
+
|
|
550
|
+
Browser and Godot runners share the paid playtest lifecycle with desktop, Unity/Unreal export, Android/iOS and custom JSON-RPC adapters. Native adapters require a locally configured automation server and remain experimental until validated against your build. See [adapter setup and validation boundaries](docs/ADAPTERS.md).
|
package/docs/ADAPTERS.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Connect a game to Ravensight Playtester
|
|
2
|
+
|
|
3
|
+
Run the CLI on the machine that can access your build. Sign in, link the project with `ravensight-playtest init --game <gameId>`, and complete its expectations brief. `run` quotes the existing persona price before registration. Adapters share the same prepaid job, action limits, model budget, cancellation, reports and uploads.
|
|
4
|
+
|
|
5
|
+
| Adapter | Target | Validation |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `playwright_web` | Browser URL, including canvas/WebGL exports | Automated tests and live Jump Bunny gameplay |
|
|
8
|
+
| `godot_driver` | Godot project with the playtest addon | Existing addon protocol and runner tests |
|
|
9
|
+
| `desktop_driver` | Local W3C/Appium session configuration | Protocol tested; experimental on physical builds |
|
|
10
|
+
| `unity_driver`, `unreal_driver` | Desktop exports through that same W3C interface | Engine-neutral screen controls, not editor plugins; experimental |
|
|
11
|
+
| `android_driver`, `ios_driver` | Appium device/emulator/simulator configuration | Protocol tested; hardware/build validation still required |
|
|
12
|
+
| `cli_stdio` | Developer-owned JSON-RPC bridge | Real child-process protocol test |
|
|
13
|
+
|
|
14
|
+
No adapter promises every game is autonomously solvable. Fast action games, unusual controls, anti-cheat, hardware access and protected platforms may require a game-specific bridge. Console tooling and authorization remain the developer's responsibility. The runner cannot bypass platform access restrictions.
|
|
15
|
+
|
|
16
|
+
## Browser and Godot
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
ravensight-playtest run --driver playwright_web --build-url https://your-game.example/ --headed
|
|
20
|
+
ravensight-playtest run --driver godot_driver --godot-project ./your-game
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`--controller jev` enables structured action selection when the server supports it. Screen interpretation proposes controls and the selector chooses a supplied action. Uncertain decisions stop for review. Provider credentials stay on Ravensight. A completed playtest report is evidence for human review, not proof that a game is bug-free.
|
|
24
|
+
|
|
25
|
+
## Desktop and mobile
|
|
26
|
+
|
|
27
|
+
Install and configure an Appium-compatible driver for your platform. Start its server locally. Supply `ravensight-driver.json` with a loopback endpoint and capabilities appropriate to the installed driver:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"endpoint": "http://127.0.0.1:4723",
|
|
32
|
+
"capabilities": {
|
|
33
|
+
"platformName": "Android",
|
|
34
|
+
"appium:automationName": "UiAutomator2",
|
|
35
|
+
"appium:deviceName": "YOUR_LOCAL_DEVICE",
|
|
36
|
+
"appium:app": "/absolute/path/to/game.apk"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
ravensight-playtest run --driver android_driver --driver-config ./ravensight-driver.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For iOS, use `ios_driver`, `platformName: iOS`, your XCUITest driver and a provisioned device or simulator. For desktop, use the capabilities required by your installed desktop driver. Unity/Unreal exports use `unity_driver`/`unreal_driver`; no engine source is uploaded by this configuration. Configuration files are local and should not contain provider credentials. Do not expose the automation server publicly.
|
|
46
|
+
|
|
47
|
+
The runner creates a W3C session, reads page source plus PNG screenshots, and executes bounded coordinate clicks, keys, text and single-pointer swipes. It releases input after each action and closes the session when done. One persona uses the device at a time. Native adapters currently capture screenshots, not video or engine logs. Browser navigation is unavailable on these adapters. Model-visible text is limited to 12,000 characters; responses are bounded to 8 MB and commands time out after 30 seconds.
|
|
48
|
+
|
|
49
|
+
## Custom bridge
|
|
50
|
+
|
|
51
|
+
Use this for text games or your own engine/hardware integration. `--driver-config` points to:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"command": "/absolute/path/to/bridge-executable",
|
|
56
|
+
"args": ["--game", "/absolute/path/to/build"],
|
|
57
|
+
"cwd": "/absolute/path/to/project"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The configured executable is trusted local code chosen by the developer. The model cannot change it. It runs without a shell. Standard output must contain newline-delimited JSON-RPC 2.0 responses. Put diagnostics on stderr. Respond with the request ID and `result`, or an `error`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{"jsonrpc":"2.0","id":1,"method":"launch","params":{"protocolVersion":1}}
|
|
65
|
+
{"jsonrpc":"2.0","id":1,"result":{"ok":true}}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Required methods:
|
|
69
|
+
|
|
70
|
+
- `launch`: initialize/reset the game; return `{ "ok": true }`.
|
|
71
|
+
- `observe`: return `{ "text": "visible state" }`, optionally `screenshotBase64` containing PNG data.
|
|
72
|
+
- `act`: accept a `kind` of `key`, `type`, `click` or `wait`; return `{ "ok": true }` after execution. Apply your own game-specific legality checks. Never interpret model text as shell code.
|
|
73
|
+
- `screenshot`: return `{ "pngBase64": "..." }`, or a JSON-RPC error if unavailable. Text-only games can still produce text reports without screenshots.
|
|
74
|
+
|
|
75
|
+
Requests expire after 30 seconds. Oversized or malformed responses stop the bridge. The runner terminates its child when the session ends; the bridge must clean up any game processes it owns on termination. No custom protocol can establish compatibility with untested hardware by itself.
|
|
76
|
+
|
|
77
|
+
Protocol references: [W3C WebDriver](https://www.w3.org/TR/webdriver/), [Appium capabilities](https://appium.io/docs/en/3.2/guides/caps/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ravensight-playtest",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "AI personas play your game and file reports to Ravensight.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://ravensight.io/docs/#playtest",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"bin",
|
|
26
26
|
"src",
|
|
27
27
|
"addons",
|
|
28
|
+
"docs/ADAPTERS.md",
|
|
28
29
|
"README.md",
|
|
29
30
|
"LICENSE",
|
|
30
31
|
"!src/**/*.test.js",
|
package/src/cli.js
CHANGED
|
@@ -50,14 +50,15 @@ async function loadRunner(name, exportName) {
|
|
|
50
50
|
* Kept in the same order as `VALUED` and `BOOLEAN` in src/run/args.js, which is
|
|
51
51
|
* the list it has to agree with.
|
|
52
52
|
*/
|
|
53
|
-
const RUNNER_HELP = `
|
|
53
|
+
export const RUNNER_HELP = `
|
|
54
54
|
Run options (parsed by the runner, see src/run/args.js):
|
|
55
55
|
--personas <list> comma separated persona slugs, default the pack's set
|
|
56
|
-
--driver <name> playwright_web or
|
|
56
|
+
--driver <name> playwright_web, godot_driver, desktop_driver, unity_driver, unreal_driver, android_driver, ios_driver or cli_stdio
|
|
57
|
+
--controller <name> standard or jev action selection (default standard)
|
|
57
58
|
--build-url <url> the web build to play
|
|
58
59
|
--godot-project <dir> the Godot project directory
|
|
59
|
-
--
|
|
60
|
-
--max-actions <n>
|
|
60
|
+
--driver-config <file> local WebDriver or JSON-RPC bridge configuration
|
|
61
|
+
--max-actions <n> lower the actions one persona may take; it can only lower the persona's own patience, never raise it
|
|
61
62
|
--concurrency <n> personas at once, default 2
|
|
62
63
|
--upload-video upload session.webm as well as the reports
|
|
63
64
|
--upload-transcript upload transcript.jsonl as well as the reports
|
|
@@ -67,7 +68,10 @@ Run options (parsed by the runner, see src/run/args.js):
|
|
|
67
68
|
--verbose print each turn
|
|
68
69
|
|
|
69
70
|
Accepted and reported as not applied by this release:
|
|
70
|
-
--
|
|
71
|
+
--quality, --offline-upload, --build-dir
|
|
72
|
+
|
|
73
|
+
There is no spending flag: each run's model budget is set by the server from
|
|
74
|
+
what the job was charged.
|
|
71
75
|
`;
|
|
72
76
|
|
|
73
77
|
/** Flags every command shares. */
|
|
@@ -156,13 +160,15 @@ export function buildProgram() {
|
|
|
156
160
|
.addOption(new Option('--modules <list>', 'price this module list').argParser(list))
|
|
157
161
|
.addOption(new Option('--personas <list>', 'price this persona list').argParser(list))
|
|
158
162
|
.option('--godot-path <path>', 'where Godot is, if it is not on the PATH')
|
|
163
|
+
.option('--web', 'check the browser even when this repo is not linked to a web build')
|
|
159
164
|
.action(capture(slot, async options => {
|
|
160
165
|
const { check } = await import('./commands/check.js');
|
|
161
166
|
return check(flagsFrom(options, {
|
|
162
167
|
game: options.game,
|
|
163
168
|
modules: options.modules,
|
|
164
169
|
personas: options.personas,
|
|
165
|
-
godotPath: options.godotPath
|
|
170
|
+
godotPath: options.godotPath,
|
|
171
|
+
web: Boolean(options.web)
|
|
166
172
|
}));
|
|
167
173
|
}));
|
|
168
174
|
|
package/src/commands/check.js
CHANGED
|
@@ -19,6 +19,75 @@ const row = (name, status, detail, fix = null) => ({ name, status, detail, fix }
|
|
|
19
19
|
/** "1 persona", "3 personas". A doctor that cannot count reads like a draft. */
|
|
20
20
|
const plural = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
21
21
|
|
|
22
|
+
/** The two commands that make the playwright_web driver work. */
|
|
23
|
+
export const BROWSER_FIX = 'npm i -D playwright && npx playwright install chromium';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The repo config, for deciding whether this is a web game. A config that
|
|
27
|
+
* cannot be read is reported by the rows that need it, not here.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} flags
|
|
30
|
+
* @returns {Promise<Object|null>}
|
|
31
|
+
*/
|
|
32
|
+
async function configForBrowser(flags) {
|
|
33
|
+
if (flags.config !== undefined) return flags.config;
|
|
34
|
+
try {
|
|
35
|
+
return await loadConfig({ repoRoot: flags.repoRoot });
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How much a missing browser matters here, or null when the row is skipped.
|
|
43
|
+
*
|
|
44
|
+
* `fail` when `--web` asks for it or the linked build is a URL: a web run
|
|
45
|
+
* cannot start without one. `warn` when only the configured driver says
|
|
46
|
+
* `playwright_web` and no build is linked yet, because that is the untouched
|
|
47
|
+
* default `init` writes, and a Godot developer who never changed it must not
|
|
48
|
+
* get exit 3 over a browser they will never launch. Skipped otherwise.
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} flags
|
|
51
|
+
* @param {Object|null} config
|
|
52
|
+
* @returns {'fail'|'warn'|null}
|
|
53
|
+
*/
|
|
54
|
+
export function browserSeverity(flags, config) {
|
|
55
|
+
if (flags.web) return 'fail';
|
|
56
|
+
if (!config) return null;
|
|
57
|
+
const kind = config.build && config.build.kind;
|
|
58
|
+
if (kind === 'url') return 'fail';
|
|
59
|
+
if (kind) return null;
|
|
60
|
+
return config.driver === 'playwright_web' ? 'warn' : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Import Playwright, launch headless Chromium, close it. The same three steps
|
|
65
|
+
* a web run starts with, so a pass here means the run will get a browser.
|
|
66
|
+
*
|
|
67
|
+
* @param {(specifier: string) => Promise<any>} [importer] test seam
|
|
68
|
+
* @returns {Promise<{ok: boolean, detail: string}>}
|
|
69
|
+
*/
|
|
70
|
+
export async function probeBrowser(importer) {
|
|
71
|
+
const load = importer || (specifier => import(specifier));
|
|
72
|
+
let browser = null;
|
|
73
|
+
try {
|
|
74
|
+
const playwright = await load('playwright');
|
|
75
|
+
browser = await playwright.chromium.launch({ headless: true });
|
|
76
|
+
const version = typeof browser.version === 'function' ? browser.version() : '';
|
|
77
|
+
return { ok: true, detail: `chromium ${version}`.trim() + ' launched headless' };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { ok: false, detail: String(error && error.message ? error.message : error).split('\n')[0] };
|
|
80
|
+
} finally {
|
|
81
|
+
if (browser) {
|
|
82
|
+
try {
|
|
83
|
+
await browser.close();
|
|
84
|
+
} catch {
|
|
85
|
+
// A browser that will not close still launched, which is what was asked.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
22
91
|
/**
|
|
23
92
|
* Every environment check, as data.
|
|
24
93
|
*
|
|
@@ -37,6 +106,7 @@ const plural = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
|
37
106
|
* @param {string[]} [flags.modules]
|
|
38
107
|
* @param {string[]} [flags.personas]
|
|
39
108
|
* @param {string} [flags.repoRoot]
|
|
109
|
+
* @param {boolean} [flags.web] probe the browser whatever the config says
|
|
40
110
|
* @param {Object} [deps]
|
|
41
111
|
* @returns {Promise<{rows: Array, ok: boolean}>}
|
|
42
112
|
*/
|
|
@@ -243,6 +313,17 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
243
313
|
'needed only to re-encode a session video before upload'
|
|
244
314
|
));
|
|
245
315
|
|
|
316
|
+
const browserNeed = browserSeverity(flags, await configForBrowser(flags));
|
|
317
|
+
if (browserNeed) {
|
|
318
|
+
const probe = await probeBrowser(deps.importer);
|
|
319
|
+
rows.push(row(
|
|
320
|
+
'browser',
|
|
321
|
+
probe.ok ? 'ok' : browserNeed,
|
|
322
|
+
probe.detail,
|
|
323
|
+
probe.ok ? null : BROWSER_FIX
|
|
324
|
+
));
|
|
325
|
+
}
|
|
326
|
+
|
|
246
327
|
const godot = await (deps.inspectGodot || inspectGodot)(
|
|
247
328
|
{ repoRoot, godotPath: flags.godotPath },
|
|
248
329
|
deps
|
package/src/commands/fix.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { appendFile, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { optionsFrom } from '../run/args.js';
|
|
4
|
+
import { optionsFrom, unknownOptionNotes } from '../run/args.js';
|
|
5
5
|
import { getDeps } from '../run/deps.js';
|
|
6
6
|
import { createDriver } from '../run/drivers/index.js';
|
|
7
7
|
import EXIT from '../run/exit.js';
|
|
@@ -15,7 +15,7 @@ import { createRepoTools, PROFILE_TOOLS } from '../run/repoTools.js';
|
|
|
15
15
|
import { assertNoSecrets } from '../run/secretScan.js';
|
|
16
16
|
import * as stateFile from '../run/state.js';
|
|
17
17
|
import { createUsageLedger, writeUsageFile } from '../run/usage.js';
|
|
18
|
-
import { createInterruptHandler, deliver, exitCodeFor, explainRegisterRefusal, formatCents, resolveSetup, resolveTarget, skillFor } from './run.js';
|
|
18
|
+
import { createInterruptHandler, deliver, driverOptions, exitCodeFor, explainRegisterRefusal, formatCents, resolveSetup, resolveTarget, skillFor } from './run.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* `ravensight-playtest fix <target>`: take one finding, change the code on a
|
|
@@ -155,6 +155,7 @@ async function fix(target, flags, injected, deps, log) {
|
|
|
155
155
|
const { options, unknown } = optionsFrom(flags);
|
|
156
156
|
if (unknown.length > 0) {
|
|
157
157
|
log(`I do not know these options: ${unknown.join(', ')}. Run "ravensight-playtest fix --help".`);
|
|
158
|
+
for (const line of unknownOptionNotes(unknown)) log(line);
|
|
158
159
|
return EXIT.ENVIRONMENT;
|
|
159
160
|
}
|
|
160
161
|
const parsed = parseTarget(target);
|
|
@@ -506,6 +507,7 @@ async function fix(target, flags, injected, deps, log) {
|
|
|
506
507
|
repoRoot,
|
|
507
508
|
entry: verifyRun,
|
|
508
509
|
pack,
|
|
510
|
+
brief,
|
|
509
511
|
finding,
|
|
510
512
|
driverName,
|
|
511
513
|
target: buildTarget,
|
|
@@ -752,6 +754,7 @@ export async function runVerify({
|
|
|
752
754
|
repoRoot,
|
|
753
755
|
entry,
|
|
754
756
|
pack,
|
|
757
|
+
brief = null,
|
|
755
758
|
finding,
|
|
756
759
|
driverName,
|
|
757
760
|
target,
|
|
@@ -769,7 +772,7 @@ export async function runVerify({
|
|
|
769
772
|
return null;
|
|
770
773
|
}
|
|
771
774
|
const headless = driverName === 'godot_driver' ? options.headless === true : options.headed !== true;
|
|
772
|
-
const driver = await makeDriver(driverName, { headless });
|
|
775
|
+
const driver = await makeDriver(driverName, driverOptions({ headless, driverName, target, directory, entry, jobId }));
|
|
773
776
|
const ledger = createUsageLedger();
|
|
774
777
|
const heartbeat = createHeartbeat({
|
|
775
778
|
api,
|
|
@@ -796,7 +799,8 @@ export async function runVerify({
|
|
|
796
799
|
target,
|
|
797
800
|
runDirectory: directory,
|
|
798
801
|
skill: skillFor(pack, 'persona_playtest'),
|
|
799
|
-
brief:
|
|
802
|
+
// The brief fetched above, as `run` does: the pack does not always carry one.
|
|
803
|
+
brief: verifyBrief(brief || pack.brief || null, finding),
|
|
800
804
|
intentNotes: pack.intent_notes || [],
|
|
801
805
|
schemas: pack.schemas || [],
|
|
802
806
|
maxActions,
|
package/src/commands/init.js
CHANGED
|
@@ -37,7 +37,8 @@ export async function ensureGitignore(repoRoot) {
|
|
|
37
37
|
/**
|
|
38
38
|
* `ravensight-playtest init --game <gameId>`
|
|
39
39
|
*
|
|
40
|
-
* Link this repo to a Ravensight game.
|
|
40
|
+
* Link this repo to a Ravensight game. Without `--game`, one visible game is
|
|
41
|
+
* used as is, and several are offered as a numbered list on a terminal.
|
|
41
42
|
*
|
|
42
43
|
* The game is checked against `whoami` before anything is written, because the
|
|
43
44
|
* alternative is a config that looks right and fails on the first job with a
|
|
@@ -45,7 +46,7 @@ export async function ensureGitignore(repoRoot) {
|
|
|
45
46
|
* authority: an unapproved game is invisible to it, by design.
|
|
46
47
|
*
|
|
47
48
|
* @param {{game?: string, apiUrl?: string, repoRoot?: string, force?: boolean, json?: boolean}} flags
|
|
48
|
-
* @param {Object} [deps]
|
|
49
|
+
* @param {{context?: Object, choose?: Function}} [deps]
|
|
49
50
|
* @returns {Promise<number>}
|
|
50
51
|
*/
|
|
51
52
|
export async function init(flags = {}, deps = {}) {
|
|
@@ -58,15 +59,39 @@ export async function init(flags = {}, deps = {}) {
|
|
|
58
59
|
// Every API call in this command goes through the one 401 handler, so a
|
|
59
60
|
// refused credential is cleared and explained here the same way it is
|
|
60
61
|
// everywhere else.
|
|
61
|
-
return withAuthHandling(context, () => linkRepo(context, flags, repoRoot));
|
|
62
|
+
return withAuthHandling(context, () => linkRepo(context, flags, repoRoot, deps.choose || ui.choose));
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* @param {Object} context
|
|
67
|
+
* @param {Object} flags
|
|
68
|
+
* @param {string} repoRoot
|
|
69
|
+
* @param {(question: string, labels: Array<string>) => Promise<number|null>} choose
|
|
70
|
+
* the picker; answers null when there is no terminal to ask on
|
|
71
|
+
*/
|
|
72
|
+
async function linkRepo(context, flags, repoRoot, choose) {
|
|
65
73
|
const whoami = await context.api.cli.whoami();
|
|
66
74
|
|
|
67
75
|
const games = whoami.games || [];
|
|
68
76
|
let gameId = flags.game;
|
|
69
77
|
|
|
78
|
+
if (!gameId && games.length === 0) {
|
|
79
|
+
throw new CliError(
|
|
80
|
+
'This token can see no games, so there is nothing to link.',
|
|
81
|
+
1,
|
|
82
|
+
{ hint: 'Create a game in the dashboard, then run login again and approve it for this token.' }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Never under --json: whatever reads that output is a script, not a person.
|
|
87
|
+
if (!gameId && games.length > 1 && !flags.json) {
|
|
88
|
+
// On a terminal, ask. Anywhere else (CI, a pipe) the picker answers null
|
|
89
|
+
// and the command falls through to the list and the --game error, which
|
|
90
|
+
// is what a script needs to see.
|
|
91
|
+
const picked = await choose(`Which game? [1-${games.length}]`, games.map(game => `${game.name || game.gameId} (${game.gameId})`));
|
|
92
|
+
if (Number.isInteger(picked) && games[picked]) gameId = games[picked].gameId;
|
|
93
|
+
}
|
|
94
|
+
|
|
70
95
|
if (!gameId) {
|
|
71
96
|
if (games.length === 1) {
|
|
72
97
|
gameId = games[0].gameId;
|
package/src/commands/profile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir } from 'node:fs/promises';
|
|
3
|
-
import { optionsFrom } from '../run/args.js';
|
|
3
|
+
import { optionsFrom, unknownOptionNotes } from '../run/args.js';
|
|
4
4
|
import { getDeps } from '../run/deps.js';
|
|
5
5
|
import EXIT from '../run/exit.js';
|
|
6
6
|
import { createHeartbeat } from '../run/heartbeat.js';
|
|
@@ -55,6 +55,7 @@ async function profile(flags, injected, deps, log) {
|
|
|
55
55
|
const { options, unknown } = optionsFrom(flags);
|
|
56
56
|
if (unknown.length > 0) {
|
|
57
57
|
log(`I do not know these options: ${unknown.join(', ')}.`);
|
|
58
|
+
for (const line of unknownOptionNotes(unknown)) log(line);
|
|
58
59
|
return EXIT.ENVIRONMENT;
|
|
59
60
|
}
|
|
60
61
|
|