ravensight-playtest 0.1.0 → 0.2.3
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 +90 -3
- package/package.json +12 -1
- package/src/api/client.js +25 -2
- package/src/cli.js +40 -1
- package/src/commands/fakeCore.js +68 -3
- package/src/commands/fix.js +1059 -0
- package/src/commands/run.js +12 -3
- package/src/dashboard.js +15 -5
- package/src/packs/index.js +17 -2
- package/src/run/args.js +6 -1
- package/src/run/fixLoop.js +489 -0
- package/src/run/git.js +331 -0
- package/src/run/index.js +7 -3
- package/src/run/model.js +7 -1
- package/src/run/repoTools.js +373 -12
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ In CI, set `RAVENSIGHT_TOKEN` instead and skip `login` entirely.
|
|
|
47
47
|
|
|
48
48
|
## Commands
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Twelve commands. Flags below are the ones the code accepts, not the ones the
|
|
51
51
|
prose elsewhere describes; `--help` on any command prints the same list.
|
|
52
52
|
|
|
53
53
|
Every command takes these four:
|
|
@@ -171,6 +171,73 @@ silently ignored: `--budget-usd` and `--quality` (the budget and the model tier
|
|
|
171
171
|
are the server's, enforced at the proxy), `--offline-upload` (uploads already
|
|
172
172
|
queue) and `--build-dir` (serve the build and pass `--build-url`).
|
|
173
173
|
|
|
174
|
+
### fix
|
|
175
|
+
|
|
176
|
+
Takes one finding, attempts the change on a branch of its own, replays the
|
|
177
|
+
finding to see whether the change worked, and prints the pull request command
|
|
178
|
+
for you to run.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
ravensight-playtest fix pt_job_1/F002 --build-url http://localhost:8080/
|
|
182
|
+
ravensight-playtest fix fnd_7g3k --no-verify
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The target is either `<jobId>/<label>`, where the label is the one the report
|
|
186
|
+
gives the finding, or a bare finding id.
|
|
187
|
+
|
|
188
|
+
| Flag | Effect |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `--game <gameId>` | The game, when this repo is not linked. |
|
|
191
|
+
| `--branch <name>` | The branch to create. Defaults to `ravensight/fix-<label>-<6 hex>`. |
|
|
192
|
+
| `--no-verify` | Do not replay the finding afterwards. The changeset then carries no verdict. |
|
|
193
|
+
| `--allow-dirty` | Fix on top of uncommitted changes instead of refusing. Your changes end up in the diff too. |
|
|
194
|
+
| `--max-actions <n>` | Cap the turns the fix may spend. Capped at 60 whatever you ask for. |
|
|
195
|
+
| `--build-url <url>`, `--godot-project <dir>`, `--driver <name>` | Where the verify run plays, resolved exactly as `run` resolves them. |
|
|
196
|
+
| `--json` | Print the changeset instead of the summary. |
|
|
197
|
+
|
|
198
|
+
What happens, in order:
|
|
199
|
+
|
|
200
|
+
1. The finding and its run report are read, with up to two of its screenshots.
|
|
201
|
+
2. Git is checked: a directory that is not a repository, or a machine with no
|
|
202
|
+
git, stops here with one sentence, and so does a working tree with
|
|
203
|
+
uncommitted changes unless you passed `--allow-dirty`.
|
|
204
|
+
3. The price is printed and confirmed. Nothing is charged and no branch is
|
|
205
|
+
created until you say yes.
|
|
206
|
+
4. The server is asked whether the finding is a bug at all. A finding it reads
|
|
207
|
+
as design intent with confidence 0.7 or more stops the attempt before
|
|
208
|
+
anything is edited.
|
|
209
|
+
5. The change is planned, then made with a write tool that can only write text
|
|
210
|
+
files inside your repository, at most 200 kB and at most 20 files.
|
|
211
|
+
6. The verify run replays the finding on the build you pointed at.
|
|
212
|
+
7. `changeset.json` and `report.md` are written, uploaded and attached to the
|
|
213
|
+
fix run, and the pull request command is printed.
|
|
214
|
+
|
|
215
|
+
**It never pushes and never commits.** There is no `git commit`, no `git push`
|
|
216
|
+
and no forge client anywhere in the command. You are left on the branch with
|
|
217
|
+
the change in your working tree, and the last thing printed is the command to
|
|
218
|
+
open the pull request once you have read the diff.
|
|
219
|
+
|
|
220
|
+
A fix attempt is one unit. It is refunded when the finding turns out to be
|
|
221
|
+
design intent, and refunded when the attempt changes nothing, in both cases
|
|
222
|
+
without a model turn being spent on the edit itself. The verify run is included
|
|
223
|
+
in the price.
|
|
224
|
+
|
|
225
|
+
**A verify can only mean something against a build this machine serves.** The
|
|
226
|
+
change exists in your working tree and nowhere else, so a `--build-url`
|
|
227
|
+
pointing at a hosted build would answer about the last upload instead. Any
|
|
228
|
+
non-local URL is refused unless you pass `--no-verify`, because a confident
|
|
229
|
+
"fixed" from the wrong build is worse than no answer. Rebuild and serve the
|
|
230
|
+
branch locally before the verify, or skip it.
|
|
231
|
+
|
|
232
|
+
The verdict is printed in words: fixed below 0.35, still reproduced above 0.65,
|
|
233
|
+
unknown in between, and "not verified" when there was no verify run.
|
|
234
|
+
|
|
235
|
+
**A fix cannot be resumed.** Ctrl-C, or cancelling the job from the dashboard,
|
|
236
|
+
abandons the attempt and gives the unit back: whatever was written stays on the
|
|
237
|
+
branch for you to read or delete, and nothing is committed. That is different
|
|
238
|
+
from `run`, which `resume` picks up where it left off, because a half-applied
|
|
239
|
+
edit is not a playthrough to carry on with.
|
|
240
|
+
|
|
174
241
|
### resume
|
|
175
242
|
|
|
176
243
|
With no job id, lists the jobs this repo has a journal for, newest first. With
|
|
@@ -299,9 +366,20 @@ In your repo:
|
|
|
299
366
|
session.webm only when you opt in
|
|
300
367
|
```
|
|
301
368
|
|
|
369
|
+
A `fix` job's run directory holds `changeset.json`, `report.md` (the pull
|
|
370
|
+
request body) and `usage.json`, plus the screenshots it was shown as evidence,
|
|
371
|
+
and a local `transcript.jsonl` of the apply loop (which tool, on what, how
|
|
372
|
+
much came back) that is never uploaded and is how an attempt that wrote
|
|
373
|
+
nothing gets explained afterwards.
|
|
374
|
+
|
|
302
375
|
Add `.ravensight/jobs/` to your `.gitignore`; `init` does it for you when you
|
|
303
376
|
already have one.
|
|
304
377
|
|
|
378
|
+
`fix` also leaves a branch on your working copy, checked out, with the change in
|
|
379
|
+
your working tree and nothing committed. It is the only thing in this CLI that
|
|
380
|
+
writes to your source at all, and it never pushes: no commit, no push, no forge
|
|
381
|
+
client exists in it. Delete the branch and you are back where you started.
|
|
382
|
+
|
|
305
383
|
## What leaves your machine
|
|
306
384
|
|
|
307
385
|
The dashboard has the full answer, artifact by artifact, at
|
|
@@ -314,8 +392,8 @@ which is the page to trust: it is rendered from the server's own allowlist, so
|
|
|
314
392
|
it cannot drift from what is actually accepted. `ravensight-playtest open` links
|
|
315
393
|
to it. The short version:
|
|
316
394
|
|
|
317
|
-
Uploaded by default: `report.md`, `report.json`, `usage.json`,
|
|
318
|
-
capability reports, and `screenshots/`. Uploaded only when you ask:
|
|
395
|
+
Uploaded by default: `report.md`, `report.json`, `usage.json`, `changeset.json`,
|
|
396
|
+
the aggregate and capability reports, and `screenshots/`. Uploaded only when you ask:
|
|
319
397
|
`transcript.jsonl` (`--upload-transcript`) and `session.webm`
|
|
320
398
|
(`--upload-video`). **Source code never, at any path.** The upload allowlist has
|
|
321
399
|
no entry for it, the CLI refuses it locally, and the server refuses anything not
|
|
@@ -325,6 +403,15 @@ pins it equal to the server's file when a checkout is on the same machine.
|
|
|
325
403
|
Findings cite a file and a line range. The source text behind that line is not
|
|
326
404
|
uploaded.
|
|
327
405
|
|
|
406
|
+
A `fix` uploads `changeset.json` and `report.md`. The changeset carries the
|
|
407
|
+
finding id, the branch name, the commit it branched from, the file paths it
|
|
408
|
+
changed with their added and removed line counts, the verify verdict, and the
|
|
409
|
+
pull request title and body. **Not the diff, and not the file contents.** What
|
|
410
|
+
the model read while working is what goes over the wire as part of its own
|
|
411
|
+
reasoning, exactly as `profile` already does, and the same guards apply: a file
|
|
412
|
+
that trips the secret scan is withheld rather than read, and both files a fix
|
|
413
|
+
writes are scanned again before they are uploaded.
|
|
414
|
+
|
|
328
415
|
Run `upload --dry-upload` or `run --dry-upload` to see the exact file list and
|
|
329
416
|
byte count before anything leaves the machine.
|
|
330
417
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ravensight-playtest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "AI personas play your game and file reports to Ravensight.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://ravensight.io/docs/#playtest",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"email": "support@realityse.com"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"godot",
|
|
12
|
+
"playtest",
|
|
13
|
+
"game-analytics",
|
|
14
|
+
"ai-personas",
|
|
15
|
+
"ravensight"
|
|
16
|
+
],
|
|
6
17
|
"type": "module",
|
|
7
18
|
"bin": {
|
|
8
19
|
"ravensight-playtest": "bin/ravensight-playtest.js"
|
package/src/api/client.js
CHANGED
|
@@ -152,9 +152,14 @@ export function createClient(options = {}) {
|
|
|
152
152
|
cacheable(`${PACKS_BASE}/skills`, { query: { modules: packModules(modules) }, etag }),
|
|
153
153
|
schemas: ({ etag } = {}) => cacheable(`${PACKS_BASE}/schemas`, { etag }),
|
|
154
154
|
routing: ({ etag } = {}) => cacheable(`${PACKS_BASE}/routing`, { etag }),
|
|
155
|
-
|
|
155
|
+
/**
|
|
156
|
+
* `jobId` asks for the bundle in the context of one job, which is the only
|
|
157
|
+
* way `graph_context` comes back: without it that field is null, because
|
|
158
|
+
* the graph is assembled from the job's own findings.
|
|
159
|
+
*/
|
|
160
|
+
bundle: ({ gameId, modules, jobId, etag } = {}) =>
|
|
156
161
|
cacheable(`${PACKS_BASE}/bundle`, {
|
|
157
|
-
query: { game_id: gameId, modules: packModules(modules) },
|
|
162
|
+
query: { game_id: gameId, modules: packModules(modules), job_id: jobId },
|
|
158
163
|
etag
|
|
159
164
|
})
|
|
160
165
|
};
|
|
@@ -243,6 +248,18 @@ export function createClient(options = {}) {
|
|
|
243
248
|
request('POST', `${gameBase(gameId)}/jobs/${encodeURIComponent(jobId)}/aggregate/complete`, { body })
|
|
244
249
|
.then(answer => answer.body),
|
|
245
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Ask whether a finding is a bug at all before spending a fix on it.
|
|
253
|
+
*
|
|
254
|
+
* Answers `{ verdict: 'bug' | 'design_intent' | 'content' | null,
|
|
255
|
+
* confidence, source }`. The verdict is the server's, from the brief's
|
|
256
|
+
* design intent notes and whatever the dashboard has been told, which is
|
|
257
|
+
* why it is a route rather than a judgement the CLI makes locally.
|
|
258
|
+
*/
|
|
259
|
+
fixTriage: (gameId, jobId, body) =>
|
|
260
|
+
request('POST', `${gameBase(gameId)}/jobs/${encodeURIComponent(jobId)}/fix/triage`, { body })
|
|
261
|
+
.then(answer => answer.body),
|
|
262
|
+
|
|
246
263
|
artifacts: (gameId, jobId) =>
|
|
247
264
|
get(`${gameBase(gameId)}/jobs/${encodeURIComponent(jobId)}/artifacts`),
|
|
248
265
|
|
|
@@ -271,6 +288,12 @@ export function createClient(options = {}) {
|
|
|
271
288
|
const runs = {
|
|
272
289
|
list: (gameId, jobId) => get(`${gameBase(gameId)}/jobs/${encodeURIComponent(jobId)}/runs`),
|
|
273
290
|
|
|
291
|
+
/**
|
|
292
|
+
* One run. `fix` reads it for `verify_verdict`, which the server attaches
|
|
293
|
+
* to a verify run once it has completed and which nothing else answers.
|
|
294
|
+
*/
|
|
295
|
+
get: (gameId, jobId, runId) => get(runPath(gameId, jobId, runId)),
|
|
296
|
+
|
|
274
297
|
add: (gameId, jobId, personas) =>
|
|
275
298
|
request('POST', `${gameBase(gameId)}/jobs/${encodeURIComponent(jobId)}/runs`, {
|
|
276
299
|
body: { personas }
|
package/src/cli.js
CHANGED
|
@@ -19,7 +19,8 @@ import { ApiError } from './api/errors.js';
|
|
|
19
19
|
/** Where each runner backed command lives, for the message below. */
|
|
20
20
|
const RUNNER_COMMANDS = {
|
|
21
21
|
run: 'src/run/index.js',
|
|
22
|
-
profile: 'src/run/index.js'
|
|
22
|
+
profile: 'src/run/index.js',
|
|
23
|
+
fix: 'src/run/index.js'
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
async function loadRunner(name, exportName) {
|
|
@@ -108,6 +109,19 @@ function flagsFrom(options, extra = {}) {
|
|
|
108
109
|
|
|
109
110
|
const list = value => value.split(',').map(part => part.trim()).filter(Boolean);
|
|
110
111
|
|
|
112
|
+
/**
|
|
113
|
+
* A copy of `values` with the first occurrence of `value` removed.
|
|
114
|
+
* @param {Array<string>} values
|
|
115
|
+
* @param {string} value
|
|
116
|
+
* @returns {Array<string>}
|
|
117
|
+
*/
|
|
118
|
+
function withoutFirst(values, value) {
|
|
119
|
+
const copy = [...values];
|
|
120
|
+
const at = copy.indexOf(value);
|
|
121
|
+
if (at !== -1) copy.splice(at, 1);
|
|
122
|
+
return copy;
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
/**
|
|
112
126
|
* Build the program. Exported rather than executed so a test can parse an
|
|
113
127
|
* argument vector without spawning a process or exiting it.
|
|
@@ -241,6 +255,31 @@ export function buildProgram() {
|
|
|
241
255
|
return profile(flagsFrom(options, { game: options.game, argv: command.args }));
|
|
242
256
|
}));
|
|
243
257
|
|
|
258
|
+
common(program.command('fix').description('attempt a fix for one finding, on a new branch'))
|
|
259
|
+
.argument('<target>', 'the finding: <jobId>/<label>, or a finding id')
|
|
260
|
+
.option('--game <gameId>', 'the game, when this repo is not linked')
|
|
261
|
+
.option('--branch <name>', 'the branch to create (default ravensight/fix-<label>-<hex>)')
|
|
262
|
+
.option('--no-verify', 'do not replay the finding after the fix')
|
|
263
|
+
.option('--allow-dirty', 'fix on top of uncommitted changes instead of refusing')
|
|
264
|
+
.allowUnknownOption()
|
|
265
|
+
// Unknown options' values land in command.args; without this commander
|
|
266
|
+
// counts them as excess positional arguments and refuses the command.
|
|
267
|
+
.allowExcessArguments()
|
|
268
|
+
.addHelpText('after', RUNNER_HELP)
|
|
269
|
+
.action(capture(slot, async (target, options, command) => {
|
|
270
|
+
const fix = await loadRunner('fix', 'fixCommand');
|
|
271
|
+
return fix(target, flagsFrom(options, {
|
|
272
|
+
game: options.game,
|
|
273
|
+
branch: options.branch,
|
|
274
|
+
verify: options.verify,
|
|
275
|
+
allowDirty: Boolean(options.allowDirty),
|
|
276
|
+
// Commander puts operands first in `command.args` and appends the
|
|
277
|
+
// options it did not recognise, so the target is in there too and only
|
|
278
|
+
// the first copy of it is the operand.
|
|
279
|
+
argv: withoutFirst(command.args, target)
|
|
280
|
+
}));
|
|
281
|
+
}));
|
|
282
|
+
|
|
244
283
|
common(program.command('run').description('run a playtest'))
|
|
245
284
|
.option('--game <gameId>', 'the game, when this repo is not linked')
|
|
246
285
|
.allowUnknownOption()
|
package/src/commands/fakeCore.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const SKILL = ['---', 'name: persona-playtest', 'prompt_version: persona-playtest@0.1.0', '---', '# Persona Playtest'].join('\n');
|
|
18
18
|
const PROFILE_SKILL = ['---', 'name: game-profile', 'prompt_version: game-profile@0.1.0', '---', '# Game Profile'].join('\n');
|
|
19
|
+
const FIX_SKILL = ['---', 'name: fix', 'prompt_version: fix@0.1.0', '---', '# Fix'].join('\n');
|
|
19
20
|
|
|
20
21
|
/** The server's table, verbatim. */
|
|
21
22
|
export const RUN_TRANSITIONS = Object.freeze({
|
|
@@ -102,7 +103,8 @@ export function fakePack(overrides = {}) {
|
|
|
102
103
|
],
|
|
103
104
|
skills: [
|
|
104
105
|
{ module: 'persona_playtest', name: 'persona-playtest', content: SKILL },
|
|
105
|
-
{ module: 'game_profile', name: 'game-profile', content: PROFILE_SKILL }
|
|
106
|
+
{ module: 'game_profile', name: 'game-profile', content: PROFILE_SKILL },
|
|
107
|
+
{ module: 'fix', name: 'fix', content: FIX_SKILL }
|
|
106
108
|
],
|
|
107
109
|
schemas: [
|
|
108
110
|
{ name: 'playtest-report', content: REPORT_SCHEMA },
|
|
@@ -186,6 +188,36 @@ export const REPORT_MD = [
|
|
|
186
188
|
''
|
|
187
189
|
].join('\n');
|
|
188
190
|
|
|
191
|
+
/**
|
|
192
|
+
* The two runs a `fix` job buys: the fix itself, and the persona run that
|
|
193
|
+
* replays the finding afterwards. There is no aggregate run, because a fix
|
|
194
|
+
* job has nothing to aggregate.
|
|
195
|
+
*/
|
|
196
|
+
export const FIX_RUNS = Object.freeze([
|
|
197
|
+
{ run_id: 'pt_run_fix', module: 'fix', persona: null, playtest_token: 'tok_fix', budget_usd: 1.5 },
|
|
198
|
+
{ run_id: 'pt_run_verify', module: 'persona_playtest', persona: 'curious-kid', playtest_token: 'tok_verify', budget_usd: 0.6 }
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {Object} [overrides]
|
|
203
|
+
* @returns {Object} one finding, the shape `GET /findings` answers with
|
|
204
|
+
*/
|
|
205
|
+
export function fakeFinding(overrides = {}) {
|
|
206
|
+
return {
|
|
207
|
+
finding_id: 'fnd_1',
|
|
208
|
+
label: 'F002',
|
|
209
|
+
job_id: 'pt_job_1',
|
|
210
|
+
run_id: 'pt_run_a',
|
|
211
|
+
persona: 'curious-kid',
|
|
212
|
+
title: 'The pause menu cannot be closed',
|
|
213
|
+
severity: 'major',
|
|
214
|
+
kind: 'bug',
|
|
215
|
+
repro_steps: ['press start', 'press escape', 'press escape again'],
|
|
216
|
+
persona_quote: 'I am stuck in this menu and nothing I press gets me out.',
|
|
217
|
+
...overrides
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
189
221
|
/**
|
|
190
222
|
* @param {Object} [options]
|
|
191
223
|
* @returns {Object} deps, with a `calls` log and a `runStates` map
|
|
@@ -198,7 +230,11 @@ export function fakeCore({
|
|
|
198
230
|
publish = true,
|
|
199
231
|
aggregateFinishes = false,
|
|
200
232
|
brief = fakeBrief(),
|
|
201
|
-
pack = fakePack()
|
|
233
|
+
pack = fakePack(),
|
|
234
|
+
findings = [fakeFinding()],
|
|
235
|
+
triage = { verdict: 'bug', confidence: 0.9, source: 'brief' },
|
|
236
|
+
graphContext = { levels: [{ name: 'title', exits: ['level_1'] }] },
|
|
237
|
+
verifyVerdict = { reproduced: 0.1, source: 'judge' }
|
|
202
238
|
} = {}) {
|
|
203
239
|
const calls = [];
|
|
204
240
|
const record = (name, payload) => calls.push({ name, ...payload });
|
|
@@ -265,6 +301,21 @@ export function fakeCore({
|
|
|
265
301
|
record('uploads', { jobId, body });
|
|
266
302
|
return { urls: body.files.map(file => ({ path: file.path, put_url: 'https://s3.test/x', tagging: null })) };
|
|
267
303
|
},
|
|
304
|
+
async findings(gameId, query) {
|
|
305
|
+
record('findings', { gameId, query });
|
|
306
|
+
// The server narrows by `job` when it is given; the fake does the same
|
|
307
|
+
// so a test cannot pass by matching a finding from another job.
|
|
308
|
+
const wanted = query && query.job;
|
|
309
|
+
return { findings: wanted ? findings.filter(entry => entry.job_id === wanted) : findings };
|
|
310
|
+
},
|
|
311
|
+
async fixTriage(gameId, jobId, body) {
|
|
312
|
+
record('fixTriage', { jobId, body });
|
|
313
|
+
return { ...triage };
|
|
314
|
+
},
|
|
315
|
+
async artifacts(gameId, jobId) {
|
|
316
|
+
record('artifacts', { jobId });
|
|
317
|
+
return { artifacts: [] };
|
|
318
|
+
},
|
|
268
319
|
async aggregateComplete(gameId, jobId, body) {
|
|
269
320
|
record('aggregateComplete', { jobId, body });
|
|
270
321
|
return aggregateFinishes
|
|
@@ -302,6 +353,15 @@ export function fakeCore({
|
|
|
302
353
|
async list(gameId, jobId) {
|
|
303
354
|
record('list', { jobId });
|
|
304
355
|
return { runs: plan.map(entry => ({ ...entry, state: runStates.get(entry.run_id), actions_taken: 0 })) };
|
|
356
|
+
},
|
|
357
|
+
async get(gameId, jobId, runId) {
|
|
358
|
+
record('runGet', { jobId, runId });
|
|
359
|
+
const entry = plan.find(candidate => candidate.run_id === runId) || { run_id: runId };
|
|
360
|
+
const state = runStates.get(runId) || 'queued';
|
|
361
|
+
// `verify_verdict` exists only on a completed verify run, which is the
|
|
362
|
+
// one thing the fix command reads this route for.
|
|
363
|
+
const verdict = entry.module === 'persona_playtest' && state === 'succeeded' ? verifyVerdict : undefined;
|
|
364
|
+
return { run: { ...entry, state, ...(verdict === undefined ? {} : { verify_verdict: verdict }) } };
|
|
305
365
|
}
|
|
306
366
|
}
|
|
307
367
|
};
|
|
@@ -325,7 +385,12 @@ export function fakeCore({
|
|
|
325
385
|
packs: {
|
|
326
386
|
async get(options) {
|
|
327
387
|
record('packs', { options });
|
|
328
|
-
|
|
388
|
+
// `graph_context` is populated only in a bundle asked for with a job
|
|
389
|
+
// id, because the server assembles it from that job's findings. A fake
|
|
390
|
+
// that answered it either way would hide a missing refetch.
|
|
391
|
+
return options && options.jobId
|
|
392
|
+
? { ...pack, graph_context: pack.graph_context ?? graphContext }
|
|
393
|
+
: { ...pack, graph_context: null };
|
|
329
394
|
}
|
|
330
395
|
},
|
|
331
396
|
async finalizeRun(dir, options) {
|