@mpgd/cli 0.9.0 → 0.10.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.
@@ -1,4 +1,5 @@
1
1
  export declare const defaultGameAcceptanceCommandTimeoutMs: number;
2
+ export declare const maximumGameplayE2EReportBytes: number;
2
3
  export declare function resolveGameAcceptanceReleaseManifestFile(gameRoot: string, env?: NodeJS.ProcessEnv): string;
3
4
  export type GameAcceptanceStatus = 'failed' | 'passed';
4
5
  export type GameAcceptanceStepStatus = 'failed' | 'passed' | 'skipped';
@@ -37,6 +38,13 @@ export interface GameAcceptanceReport {
37
38
  readonly parseError: string | null;
38
39
  readonly value: unknown;
39
40
  } | null;
41
+ readonly gameplayE2E: {
42
+ readonly file: string;
43
+ readonly found: boolean;
44
+ readonly parseError: string | null;
45
+ readonly validationError: string | null;
46
+ readonly value: unknown;
47
+ } | null;
40
48
  };
41
49
  }
42
50
  export interface GameAcceptanceCommandResult {
@@ -48,6 +56,12 @@ export interface RunGameAcceptanceInput {
48
56
  readonly gameRoot: string;
49
57
  readonly reportDir: string;
50
58
  readonly releaseManifestFile?: string;
59
+ readonly gameplayE2EReportFile?: string;
60
+ readonly requireGameplayE2EReport?: boolean;
61
+ /** Step that must replace any previous gameplay report before evidence is accepted. */
62
+ readonly gameplayE2EStepId?: string;
63
+ /** Resolved acceptance targets that gameplay evidence is allowed to represent. */
64
+ readonly gameplayE2ETargets?: readonly string[];
51
65
  readonly options: Readonly<Record<string, string | boolean | null>>;
52
66
  readonly steps: readonly GameAcceptanceStep[];
53
67
  readonly env?: NodeJS.ProcessEnv;
@@ -1,8 +1,10 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { closeSync, existsSync, mkdirSync, openSync, readSync, rmSync, writeFileSync, } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { collectGameplayE2EPathEvidence, maximumGameplayE2EStates, readGameplayE2EPlan, resolveGameplayE2EPathInsideGameRoot, } from './gameplay-e2e.js';
4
5
  export const defaultGameAcceptanceCommandTimeoutMs = 30 * 60 * 1000;
5
6
  const defaultGameAcceptanceReleaseManifestFile = 'artifacts/release-manifest.json';
7
+ export const maximumGameplayE2EReportBytes = 1024 * 1024;
6
8
  export function resolveGameAcceptanceReleaseManifestFile(gameRoot, env = process.env) {
7
9
  const configuredFile = env.MPGD_RELEASE_MANIFEST_FILE;
8
10
  return path.resolve(gameRoot, configuredFile === undefined || configuredFile.length === 0
@@ -22,7 +24,31 @@ export function runGameAcceptance(input) {
22
24
  const startedAtMs = now();
23
25
  const results = [];
24
26
  let failed = false;
27
+ let gameplayE2EReportFileToReplace;
28
+ if (input.gameplayE2EStepId !== undefined) {
29
+ if (input.gameplayE2EReportFile === undefined) {
30
+ throw new Error('A gameplay E2E evidence step needs a report file.');
31
+ }
32
+ if (input.steps.filter((step) => step.id === input.gameplayE2EStepId).length !== 1) {
33
+ throw new Error(`Gameplay E2E evidence step must occur exactly once: ${input.gameplayE2EStepId}`);
34
+ }
35
+ gameplayE2EReportFileToReplace = resolveGameplayE2EPathInsideGameRoot(gameRoot, input.gameplayE2EReportFile, 'Gameplay E2E report');
36
+ }
25
37
  for (const step of input.steps) {
38
+ if (!failed
39
+ && step.id === input.gameplayE2EStepId
40
+ && gameplayE2EReportFileToReplace !== undefined) {
41
+ try {
42
+ // Earlier acceptance commands are untrusted and may have changed the path hierarchy.
43
+ const verifiedGameplayE2EReportFile = resolveGameplayE2EPathInsideGameRoot(gameRoot, gameplayE2EReportFileToReplace, 'Gameplay E2E report');
44
+ rmSync(verifiedGameplayE2EReportFile, { force: true });
45
+ }
46
+ catch (error) {
47
+ results.push(failedStepResult(step, now(), formatError(error)));
48
+ failed = true;
49
+ continue;
50
+ }
51
+ }
26
52
  if (failed) {
27
53
  results.push(skippedStepResult(step, now(), 'A previous acceptance step failed.'));
28
54
  continue;
@@ -69,18 +95,25 @@ export function runGameAcceptance(input) {
69
95
  });
70
96
  failed = status === 'failed';
71
97
  }
72
- const finishedAtMs = now();
73
98
  const releaseManifest = readOptionalJsonEvidence(input.releaseManifestFile, gameRoot);
99
+ const gameplayE2E = readGameplayE2EEvidence(input.gameplayE2EReportFile, gameRoot, input.releaseManifestFile, typeof input.options.profile === 'string' ? input.options.profile : undefined, input.gameplayE2ETargets);
100
+ const evidenceFailed = input.requireGameplayE2EReport === true
101
+ && (gameplayE2E === null
102
+ || !gameplayE2E.found
103
+ || gameplayE2E.parseError !== null
104
+ || gameplayE2E.validationError !== null);
105
+ const finishedAtMs = now();
74
106
  const report = {
75
107
  schemaVersion: 1,
76
108
  generatedAt: new Date(finishedAtMs).toISOString(),
77
- status: failed ? 'failed' : 'passed',
109
+ status: failed || evidenceFailed ? 'failed' : 'passed',
78
110
  gameRoot,
79
111
  durationMs: Math.max(0, finishedAtMs - startedAtMs),
80
112
  options: input.options,
81
113
  steps: results,
82
114
  evidence: {
83
115
  releaseManifest,
116
+ gameplayE2E,
84
117
  },
85
118
  };
86
119
  const reportDir = path.resolve(input.reportDir);
@@ -126,6 +159,22 @@ export function renderGameAcceptanceMarkdown(report) {
126
159
  else {
127
160
  lines.push(`- Release manifest: ${escapeMarkdownInline(report.evidence.releaseManifest.file)}`);
128
161
  }
162
+ lines.push('', '## Gameplay E2E Evidence', '');
163
+ if (report.evidence.gameplayE2E === null) {
164
+ lines.push('- Gameplay E2E evidence collection disabled.');
165
+ }
166
+ else if (!report.evidence.gameplayE2E.found) {
167
+ lines.push(`- Gameplay E2E report not found: ${escapeMarkdownInline(report.evidence.gameplayE2E.file)}`);
168
+ }
169
+ else if (report.evidence.gameplayE2E.parseError !== null) {
170
+ lines.push(`- Gameplay E2E report is invalid JSON: ${escapeMarkdownInline(report.evidence.gameplayE2E.file)}`, ` - ${escapeMarkdownInline(report.evidence.gameplayE2E.parseError)}`);
171
+ }
172
+ else if (report.evidence.gameplayE2E.validationError !== null) {
173
+ lines.push(`- Gameplay E2E report failed validation: ${escapeMarkdownInline(report.evidence.gameplayE2E.file)}`, ` - ${escapeMarkdownInline(report.evidence.gameplayE2E.validationError)}`);
174
+ }
175
+ else {
176
+ lines.push(`- Gameplay E2E report: ${escapeMarkdownInline(report.evidence.gameplayE2E.file)}`);
177
+ }
129
178
  return `${lines.join('\n')}\n`;
130
179
  }
131
180
  function runAcceptanceCommand(step, env, timeoutMs) {
@@ -188,32 +237,296 @@ function readOptionalJsonEvidence(file, gameRoot) {
188
237
  if (file === undefined) {
189
238
  return null;
190
239
  }
191
- const resolved = path.resolve(gameRoot, file);
240
+ let resolved;
241
+ try {
242
+ resolved = resolveGameplayE2EPathInsideGameRoot(gameRoot, file, 'Release manifest');
243
+ }
244
+ catch (error) {
245
+ return {
246
+ file: '<outside-game-root>',
247
+ found: false,
248
+ parseError: formatError(error),
249
+ value: null,
250
+ };
251
+ }
192
252
  const displayFile = relativeOrAbsolute(gameRoot, resolved);
193
253
  if (!existsSync(resolved)) {
194
254
  return { file: displayFile, found: false, parseError: null, value: null };
195
255
  }
196
256
  try {
257
+ const releaseManifestContent = readBoundedUtf8File(resolved, maximumGameplayE2EReportBytes);
258
+ if (releaseManifestContent === null) {
259
+ return {
260
+ file: displayFile,
261
+ found: true,
262
+ parseError: `Release manifest cannot exceed ${maximumGameplayE2EReportBytes} bytes.`,
263
+ value: null,
264
+ };
265
+ }
266
+ return {
267
+ file: displayFile,
268
+ found: true,
269
+ parseError: null,
270
+ value: JSON.parse(releaseManifestContent),
271
+ };
272
+ }
273
+ catch (error) {
197
274
  return {
198
275
  file: displayFile,
199
276
  found: true,
277
+ parseError: formatError(error),
278
+ value: null,
279
+ };
280
+ }
281
+ }
282
+ function readGameplayE2EEvidence(file, gameRoot, releaseManifestFile, expectedProfile, expectedTargets) {
283
+ if (file === undefined) {
284
+ return null;
285
+ }
286
+ let resolved;
287
+ try {
288
+ resolved = resolveGameplayE2EPathInsideGameRoot(gameRoot, file, 'Gameplay E2E report');
289
+ }
290
+ catch (error) {
291
+ return {
292
+ file: '<outside-game-root>',
293
+ found: false,
294
+ parseError: null,
295
+ validationError: formatError(error),
296
+ value: null,
297
+ };
298
+ }
299
+ const displayFile = relativeOrAbsolute(gameRoot, resolved);
300
+ if (!existsSync(resolved)) {
301
+ return {
302
+ file: displayFile,
303
+ found: false,
200
304
  parseError: null,
201
- value: JSON.parse(readFileSync(resolved, 'utf8')),
305
+ validationError: null,
306
+ value: null,
202
307
  };
203
308
  }
309
+ let value;
310
+ try {
311
+ const reportContent = readBoundedUtf8File(resolved, maximumGameplayE2EReportBytes);
312
+ if (reportContent === null) {
313
+ return {
314
+ file: displayFile,
315
+ found: true,
316
+ parseError: null,
317
+ validationError: `Gameplay E2E report cannot exceed ${maximumGameplayE2EReportBytes} bytes.`,
318
+ value: null,
319
+ };
320
+ }
321
+ value = JSON.parse(reportContent);
322
+ }
204
323
  catch (error) {
205
324
  return {
206
325
  file: displayFile,
207
326
  found: true,
208
327
  parseError: formatError(error),
328
+ validationError: null,
209
329
  value: null,
210
330
  };
211
331
  }
332
+ return {
333
+ file: displayFile,
334
+ found: true,
335
+ parseError: null,
336
+ validationError: validateGameplayE2EEvidence(value, gameRoot, releaseManifestFile, expectedProfile, expectedTargets),
337
+ value,
338
+ };
339
+ }
340
+ function validateGameplayE2EEvidence(value, gameRoot, releaseManifestFile, expectedProfile, expectedTargets) {
341
+ if (!isRecord(value)) {
342
+ return 'Gameplay E2E report must be an object.';
343
+ }
344
+ if (value.schemaVersion !== 1) {
345
+ return 'Gameplay E2E report schemaVersion must be 1.';
346
+ }
347
+ if (value.status !== 'passed') {
348
+ return 'Gameplay E2E report status must be passed.';
349
+ }
350
+ const generatedAtMs = typeof value.generatedAt === 'string'
351
+ ? Date.parse(value.generatedAt)
352
+ : Number.NaN;
353
+ if (typeof value.generatedAt !== 'string'
354
+ || !Number.isFinite(generatedAtMs)
355
+ || new Date(generatedAtMs).toISOString() !== value.generatedAt) {
356
+ return 'Gameplay E2E report generatedAt must be an ISO timestamp.';
357
+ }
358
+ if (typeof value.target !== 'string' || value.target.length === 0) {
359
+ return 'Gameplay E2E report target must be a non-empty string.';
360
+ }
361
+ if (expectedTargets !== undefined && !expectedTargets.includes(value.target)) {
362
+ return `Gameplay E2E report target must match an acceptance target: ${expectedTargets.join(', ')}.`;
363
+ }
364
+ if (typeof value.profile !== 'string' || value.profile.length === 0) {
365
+ return 'Gameplay E2E report profile must be a non-empty string.';
366
+ }
367
+ if (expectedProfile !== undefined && value.profile !== expectedProfile) {
368
+ return `Gameplay E2E report profile must match acceptance profile ${expectedProfile}.`;
369
+ }
370
+ if (!isPathEvidence(value.artifact) || value.artifact.kind === 'symbolic-link') {
371
+ return 'Gameplay E2E report must link a hashed file or directory target artifact.';
372
+ }
373
+ const artifactError = validateCurrentPathEvidence(value.artifact, gameRoot, 'target artifact');
374
+ if (artifactError !== null) {
375
+ return artifactError;
376
+ }
377
+ if (!isPathEvidence(value.plan) || value.plan.kind !== 'file') {
378
+ return 'Gameplay E2E report must link its hashed manifest plan.';
379
+ }
380
+ const planError = validateCurrentPathEvidence(value.plan, gameRoot, 'manifest plan');
381
+ if (planError !== null) {
382
+ return planError;
383
+ }
384
+ let configuredPlan;
385
+ try {
386
+ configuredPlan = readGameplayE2EPlan(gameRoot);
387
+ }
388
+ catch (error) {
389
+ return `Gameplay E2E manifest plan is invalid: ${formatError(error)}`;
390
+ }
391
+ if (configuredPlan === null) {
392
+ return 'Gameplay E2E manifest plan must define acceptance.gameplay.';
393
+ }
394
+ if (path.resolve(gameRoot, value.plan.file) !== configuredPlan.file) {
395
+ return 'Gameplay E2E report must link the game manifest plan path.';
396
+ }
397
+ if (releaseManifestFile !== undefined) {
398
+ if (!isPathEvidence(value.releaseManifest) || value.releaseManifest.kind !== 'file') {
399
+ return 'Gameplay E2E report must link the current release manifest.';
400
+ }
401
+ let resolvedReleaseManifest;
402
+ try {
403
+ resolvedReleaseManifest = resolveGameplayE2EPathInsideGameRoot(gameRoot, releaseManifestFile, 'Gameplay E2E release manifest');
404
+ }
405
+ catch (error) {
406
+ return formatError(error);
407
+ }
408
+ if (path.resolve(gameRoot, value.releaseManifest.file) !== resolvedReleaseManifest) {
409
+ return 'Gameplay E2E report must link the acceptance release manifest path.';
410
+ }
411
+ const releaseManifestError = validateCurrentPathEvidence(value.releaseManifest, gameRoot, 'release manifest');
412
+ if (releaseManifestError !== null) {
413
+ return releaseManifestError;
414
+ }
415
+ let releaseManifest;
416
+ try {
417
+ const releaseManifestContent = readBoundedUtf8File(resolvedReleaseManifest, maximumGameplayE2EReportBytes);
418
+ if (releaseManifestContent === null) {
419
+ return `Gameplay E2E release manifest cannot exceed ${maximumGameplayE2EReportBytes} bytes.`;
420
+ }
421
+ releaseManifest = JSON.parse(releaseManifestContent);
422
+ }
423
+ catch (error) {
424
+ return `Gameplay E2E report release manifest is unreadable: ${formatError(error)}`;
425
+ }
426
+ const releaseTargetError = validateReleaseTargetEvidence(releaseManifest, value.target, value.profile, value.artifact.file, gameRoot);
427
+ if (releaseTargetError !== null) {
428
+ return releaseTargetError;
429
+ }
430
+ }
431
+ if (!Array.isArray(value.states) || value.states.length === 0) {
432
+ return 'Gameplay E2E report must contain at least one state.';
433
+ }
434
+ if (value.states.length > maximumGameplayE2EStates) {
435
+ return `Gameplay E2E report cannot contain more than ${maximumGameplayE2EStates} states.`;
436
+ }
437
+ const stateIds = [];
438
+ for (const state of value.states) {
439
+ if (!isRecord(state)
440
+ || typeof state.id !== 'string'
441
+ || state.id.length === 0
442
+ || state.status !== 'passed'
443
+ || !isPathEvidence(state.screenshot)
444
+ || state.screenshot.kind !== 'file') {
445
+ return 'Every gameplay E2E state must have passed with a hashed screenshot.';
446
+ }
447
+ const screenshotError = validateCurrentPathEvidence(state.screenshot, gameRoot, 'state screenshot');
448
+ if (screenshotError !== null) {
449
+ return screenshotError;
450
+ }
451
+ stateIds.push(state.id);
452
+ }
453
+ const configuredStateIds = configuredPlan.plan.states.map((state) => state.id);
454
+ if (stateIds.length !== configuredStateIds.length
455
+ || stateIds.some((stateId, index) => stateId !== configuredStateIds[index])) {
456
+ return 'Gameplay E2E report must cover every manifest plan state in order.';
457
+ }
458
+ return null;
459
+ }
460
+ function validateReleaseTargetEvidence(value, target, profile, artifactFile, gameRoot) {
461
+ if (!isRecord(value) || !isRecord(value.targets)) {
462
+ return 'Gameplay E2E release manifest must define target evidence.';
463
+ }
464
+ const targetEvidence = value.targets[target];
465
+ if (!isRecord(targetEvidence) || typeof targetEvidence.artifact !== 'string') {
466
+ return `Gameplay E2E release manifest does not contain target ${target}.`;
467
+ }
468
+ if (targetEvidence.profile !== undefined
469
+ && targetEvidence.profile !== profile) {
470
+ return `Gameplay E2E release manifest target ${target} does not match profile ${profile}.`;
471
+ }
472
+ if (path.resolve(gameRoot, targetEvidence.artifact)
473
+ !== path.resolve(gameRoot, artifactFile)) {
474
+ return `Gameplay E2E release manifest target ${target} does not match the tested artifact.`;
475
+ }
476
+ return null;
477
+ }
478
+ function isPathEvidence(value) {
479
+ return isRecord(value)
480
+ && typeof value.file === 'string'
481
+ && value.file.length > 0
482
+ && (value.kind === 'file' || value.kind === 'directory' || value.kind === 'symbolic-link')
483
+ && typeof value.sha256 === 'string'
484
+ && /^[a-f0-9]{64}$/u.test(value.sha256);
485
+ }
486
+ function isRecord(value) {
487
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
488
+ }
489
+ function validateCurrentPathEvidence(evidence, gameRoot, label) {
490
+ const resolved = path.resolve(gameRoot, evidence.file);
491
+ const relative = path.relative(gameRoot, resolved);
492
+ if (relative === '..'
493
+ || relative.startsWith(`..${path.sep}`)
494
+ || path.isAbsolute(relative)) {
495
+ return `Gameplay E2E ${label} path escapes the game root.`;
496
+ }
497
+ let current;
498
+ try {
499
+ current = collectGameplayE2EPathEvidence(gameRoot, evidence.file, label);
500
+ }
501
+ catch (error) {
502
+ return `Gameplay E2E ${label} is unavailable: ${formatError(error)}`;
503
+ }
504
+ return current.kind === evidence.kind && current.sha256 === evidence.sha256
505
+ ? null
506
+ : `Gameplay E2E ${label} hash does not match its current contents.`;
212
507
  }
213
508
  function relativeOrAbsolute(root, file) {
214
509
  const relative = path.relative(root, file);
215
510
  return relative.startsWith('..') || path.isAbsolute(relative) ? file : relative || '.';
216
511
  }
512
+ function readBoundedUtf8File(file, maximumBytes) {
513
+ const buffer = Buffer.allocUnsafe(maximumBytes + 1);
514
+ const fileDescriptor = openSync(file, 'r');
515
+ let offset = 0;
516
+ try {
517
+ while (offset < buffer.length) {
518
+ const bytesRead = readSync(fileDescriptor, buffer, offset, buffer.length - offset, null);
519
+ if (bytesRead === 0) {
520
+ break;
521
+ }
522
+ offset += bytesRead;
523
+ }
524
+ }
525
+ finally {
526
+ closeSync(fileDescriptor);
527
+ }
528
+ return offset > maximumBytes ? null : buffer.subarray(0, offset).toString('utf8');
529
+ }
217
530
  function resolveRunnableStep(step) {
218
531
  if (step.command === undefined || step.command.length === 0) {
219
532
  return { ok: false, detail: `Acceptance step ${step.id} is missing command.` };
@@ -0,0 +1,121 @@
1
+ export declare const defaultGameplayE2EReportFile = "artifacts/gameplay-e2e/gameplay-e2e-report.json";
2
+ export declare const maximumGameplayE2EStates = 50;
3
+ export interface GameplayE2EHashLimits {
4
+ readonly maximumDepth: number;
5
+ readonly maximumEntries: number;
6
+ readonly maximumTotalFileBytes: number;
7
+ }
8
+ export interface GameplayE2EPlan {
9
+ readonly schemaVersion: 1;
10
+ readonly states: readonly GameplayE2EState[];
11
+ }
12
+ export interface GameplayE2EState {
13
+ readonly id: string;
14
+ readonly label: string;
15
+ readonly expectation?: string;
16
+ readonly actions: readonly GameplayE2EAction[];
17
+ }
18
+ export type GameplayE2EAction = GameplayE2EInputAction | GameplayE2EPauseResumeAction;
19
+ export type GameplayE2EInputAction = {
20
+ readonly type: 'tap';
21
+ readonly x: number;
22
+ readonly y: number;
23
+ } | {
24
+ readonly type: 'key';
25
+ readonly key: string;
26
+ } | {
27
+ readonly type: 'wait';
28
+ readonly durationMs: number;
29
+ };
30
+ export interface GameplayE2EPauseResumeAction {
31
+ readonly type: 'pause-resume';
32
+ readonly backgroundMs: number;
33
+ readonly expectSameSession?: boolean;
34
+ }
35
+ export interface GameplayE2EObservation {
36
+ readonly passed: boolean;
37
+ readonly sessionId: string | null;
38
+ readonly detail?: string;
39
+ readonly metadata?: Readonly<Record<string, string | number | boolean | null>>;
40
+ }
41
+ export interface GameplayE2EDriver {
42
+ readonly perform: (action: GameplayE2EInputAction) => Promise<void>;
43
+ readonly pause: () => Promise<void>;
44
+ readonly resume: () => Promise<void>;
45
+ readonly inspect: (input: {
46
+ readonly state: GameplayE2EState;
47
+ readonly phase: 'after' | 'before' | 'resumed';
48
+ }) => Promise<GameplayE2EObservation>;
49
+ readonly captureScreenshot: (input: {
50
+ readonly state: GameplayE2EState;
51
+ readonly file: string;
52
+ }) => Promise<void>;
53
+ }
54
+ export type GameplayE2EStatus = 'failed' | 'passed';
55
+ export type GameplayE2EResultStatus = GameplayE2EStatus | 'skipped';
56
+ export interface GameplayE2EActionResult {
57
+ readonly action: GameplayE2EAction;
58
+ readonly status: GameplayE2EResultStatus;
59
+ readonly startedAt: string;
60
+ readonly durationMs: number;
61
+ readonly detail: string | null;
62
+ }
63
+ export interface GameplayE2EPathEvidence {
64
+ readonly file: string;
65
+ readonly kind: 'directory' | 'file' | 'symbolic-link';
66
+ readonly sha256: string;
67
+ }
68
+ export interface GameplayE2EStateResult {
69
+ readonly id: string;
70
+ readonly label: string;
71
+ readonly expectation: string | null;
72
+ readonly status: GameplayE2EResultStatus;
73
+ readonly startedAt: string;
74
+ readonly durationMs: number;
75
+ readonly detail: string | null;
76
+ readonly actions: readonly GameplayE2EActionResult[];
77
+ readonly observation: GameplayE2EObservation | null;
78
+ readonly screenshot: GameplayE2EPathEvidence | null;
79
+ }
80
+ export interface GameplayE2EReport {
81
+ readonly schemaVersion: 1;
82
+ readonly generatedAt: string;
83
+ readonly status: GameplayE2EStatus;
84
+ readonly gameRoot: string;
85
+ readonly target: string;
86
+ readonly profile: string;
87
+ readonly durationMs: number;
88
+ readonly plan: GameplayE2EPathEvidence;
89
+ readonly artifact: GameplayE2EPathEvidence;
90
+ readonly releaseManifest: GameplayE2EPathEvidence | null;
91
+ readonly states: readonly GameplayE2EStateResult[];
92
+ }
93
+ export interface RunGameplayE2EInput {
94
+ readonly gameRoot: string;
95
+ readonly reportDir: string;
96
+ readonly reportFile?: string;
97
+ readonly plan: GameplayE2EPlan;
98
+ readonly planFile: string;
99
+ readonly target: string;
100
+ readonly profile: string;
101
+ readonly artifactFile: string;
102
+ readonly releaseManifestFile?: string;
103
+ readonly driver: GameplayE2EDriver;
104
+ readonly now?: () => number;
105
+ readonly log?: (message: string) => void;
106
+ }
107
+ export interface RunGameplayE2EResult {
108
+ readonly report: GameplayE2EReport;
109
+ readonly jsonFile: string;
110
+ readonly markdownFile: string;
111
+ }
112
+ export declare function resolveGameplayE2EReportFile(gameRoot: string, env?: NodeJS.ProcessEnv): string;
113
+ export declare function readGameplayE2EPlan(gameRoot: string, configFile?: string): {
114
+ readonly file: string;
115
+ readonly plan: GameplayE2EPlan;
116
+ } | null;
117
+ export declare function parseGameplayE2EPlan(value: unknown, source?: string): GameplayE2EPlan;
118
+ export declare function runGameplayE2E(input: RunGameplayE2EInput): Promise<RunGameplayE2EResult>;
119
+ export declare function renderGameplayE2EMarkdown(report: GameplayE2EReport): string;
120
+ export declare function collectGameplayE2EPathEvidence(gameRoot: string, file: string, label: string, limits?: GameplayE2EHashLimits): GameplayE2EPathEvidence;
121
+ export declare function resolveGameplayE2EPathInsideGameRoot(gameRoot: string, file: string, label: string, allowSymbolicLinkLeaf?: boolean): string;