game-harness 1.0.0

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.
Files changed (69) hide show
  1. package/AGENTS.md +140 -0
  2. package/CHANGELOG.md +69 -0
  3. package/LICENSE +21 -0
  4. package/README.md +568 -0
  5. package/bin/test-harness-visual-battery.mjs +6 -0
  6. package/dist/cjs/bin/visual-battery.js +43 -0
  7. package/dist/cjs/browser-config.js +83 -0
  8. package/dist/cjs/chromium-launch.js +37 -0
  9. package/dist/cjs/index.js +10 -0
  10. package/dist/cjs/lighthouse.js +73 -0
  11. package/dist/cjs/package.json +3 -0
  12. package/dist/cjs/playwright-config.js +303 -0
  13. package/dist/cjs/production-runtime.js +315 -0
  14. package/dist/cjs/release-ladder.js +40 -0
  15. package/dist/cjs/silent-qa.js +75 -0
  16. package/dist/cjs/visual-battery.js +247 -0
  17. package/dist/esm/bin/visual-battery.js +41 -0
  18. package/dist/esm/browser-config.js +80 -0
  19. package/dist/esm/chromium-launch.js +34 -0
  20. package/dist/esm/index.js +3 -0
  21. package/dist/esm/lighthouse.js +70 -0
  22. package/dist/esm/playwright-config.js +292 -0
  23. package/dist/esm/production-runtime.js +304 -0
  24. package/dist/esm/release-ladder.js +37 -0
  25. package/dist/esm/silent-qa.js +67 -0
  26. package/dist/esm/visual-battery.js +239 -0
  27. package/dist/types/bin/visual-battery.d.cts +2 -0
  28. package/dist/types/bin/visual-battery.d.ts +2 -0
  29. package/dist/types/browser-config.d.cts +100 -0
  30. package/dist/types/browser-config.d.ts +100 -0
  31. package/dist/types/chromium-launch.d.cts +27 -0
  32. package/dist/types/chromium-launch.d.ts +27 -0
  33. package/dist/types/index.d.cts +3 -0
  34. package/dist/types/index.d.ts +3 -0
  35. package/dist/types/lighthouse.d.cts +44 -0
  36. package/dist/types/lighthouse.d.ts +44 -0
  37. package/dist/types/playwright-config.d.cts +122 -0
  38. package/dist/types/playwright-config.d.ts +122 -0
  39. package/dist/types/production-runtime.d.cts +105 -0
  40. package/dist/types/production-runtime.d.ts +105 -0
  41. package/dist/types/release-ladder.d.cts +38 -0
  42. package/dist/types/release-ladder.d.ts +38 -0
  43. package/dist/types/silent-qa.d.cts +47 -0
  44. package/dist/types/silent-qa.d.ts +47 -0
  45. package/dist/types/visual-battery.d.cts +69 -0
  46. package/dist/types/visual-battery.d.ts +69 -0
  47. package/docs/404.md +17 -0
  48. package/docs/agent-guide.md +10 -0
  49. package/docs/architecture.md +81 -0
  50. package/docs/assets/game-harness-hero.webp +0 -0
  51. package/docs/changelog.md +9 -0
  52. package/docs/contributing.md +21 -0
  53. package/docs/entry-points.md +41 -0
  54. package/docs/getting-started.md +42 -0
  55. package/docs/guides/chromium-and-silent-qa.md +72 -0
  56. package/docs/guides/lighthouse-and-release-ladder.md +62 -0
  57. package/docs/guides/playwright.md +58 -0
  58. package/docs/guides/production-runtime.md +61 -0
  59. package/docs/guides/visual-battery.md +58 -0
  60. package/docs/guides/vitest.md +41 -0
  61. package/docs/introduction.md +29 -0
  62. package/docs/package.json +13 -0
  63. package/docs/quick-start.md +47 -0
  64. package/docs/reference/troubleshooting.md +33 -0
  65. package/docs/security.md +15 -0
  66. package/docs/sourcey.config.ts +79 -0
  67. package/llms.txt +32 -0
  68. package/maestro/smoke.template.yaml +11 -0
  69. package/package.json +196 -0
@@ -0,0 +1,105 @@
1
+ import { type Browser, chromium, type Page } from '@playwright/test';
2
+ import { type ChromiumGpuMode } from './chromium-launch.js';
3
+ import { type OpenSilentGameOptions, type SilentQueryValue } from './playwright-config.js';
4
+ type BrowserLaunchOptions = NonNullable<Parameters<typeof chromium.launch>[0]>;
5
+ type BrowserPageOptions = Parameters<Browser['newPage']>[0];
6
+ export interface ProductionRuntimeServerOptions {
7
+ /** Executable to launch directly. Shell command strings are intentionally unsupported. */
8
+ command: string;
9
+ /** Arguments passed directly to `command`. Use the server's strict-port option. */
10
+ args?: readonly string[];
11
+ /** Child working directory. Defaults to the current process directory. */
12
+ cwd?: string;
13
+ /** Environment additions or overrides for the child process. */
14
+ env?: Readonly<NodeJS.ProcessEnv>;
15
+ /** URL polled for readiness. Defaults to the runtime `url`. */
16
+ readyUrl?: string;
17
+ /** Maximum startup time. Defaults to 15 seconds. */
18
+ startupTimeoutMs?: number;
19
+ /** Graceful shutdown time before SIGKILL. Defaults to 5 seconds. */
20
+ shutdownTimeoutMs?: number;
21
+ }
22
+ export interface ProductionRuntimeOptions {
23
+ /** Production artifact or exact-live URL to verify. */
24
+ url: string;
25
+ /** Optional owned preview server. Existing processes are never reused. */
26
+ server?: ProductionRuntimeServerOptions;
27
+ /** Chromium options. `--mute-audio` is always de-duplicated and applied last. */
28
+ browserLaunchOptions?: BrowserLaunchOptions;
29
+ /** Renderer profile. Defaults to native Chromium selection (`auto`). */
30
+ gpuMode?: ChromiumGpuMode;
31
+ /** Options for the fresh browser page. */
32
+ pageOptions?: BrowserPageOptions;
33
+ /** Extra query parameters composed with the mandatory runtime mute. */
34
+ silentParameters?: Readonly<Record<string, SilentQueryValue>>;
35
+ /** Silent marker/query overrides. The marker timeout defaults to 15 seconds here. */
36
+ silentOptions?: OpenSilentGameOptions;
37
+ /** Values written before application code and required to remain byte-for-byte unchanged. */
38
+ localStorageSentinels?: Readonly<Record<string, string | null>>;
39
+ /** Required game-specific identity plus primary UI/canvas assertions. */
40
+ assertReady: (page: Page) => Promise<void>;
41
+ /** Optional engine-specific assertion such as `window.Howler._muted === true`. */
42
+ assertSilentState?: (page: Page) => Promise<void>;
43
+ /** Brief post-ready observation window for deferred runtime errors. Defaults to 250 ms. */
44
+ settleTimeMs?: number;
45
+ }
46
+ /** Evidence returned by a successful `verifyProductionRuntime()` call. */
47
+ export interface ProductionRuntimeResult {
48
+ /** The page's URL after navigation, including the applied silent-QA query parameters. */
49
+ finalUrl: string;
50
+ /** Every requested `localStorageSentinels` key, read back after the boot completed. */
51
+ localStorage: Record<string, string | null>;
52
+ }
53
+ /**
54
+ * One runtime failure captured while the game booted: an uncaught page
55
+ * error, a `console.error` call, a network request that failed outright, or
56
+ * an HTTP response with a 4xx/5xx status. `verifyProductionRuntime()`
57
+ * accumulates these and fails closed if any were recorded.
58
+ */
59
+ export interface ProductionRuntimeIssue {
60
+ kind: 'console' | 'http' | 'pageerror' | 'requestfailed';
61
+ message: string;
62
+ /** The request/response URL, present for `'http'` and `'requestfailed'` issues. */
63
+ url?: string;
64
+ }
65
+ export interface AvailableProductionPortOptions {
66
+ /** Loopback interface used by the owned preview server. Defaults to IPv4 localhost. */
67
+ host?: string;
68
+ }
69
+ /**
70
+ * Thrown by every failure path in this module — an unreachable/occupied
71
+ * readiness URL, a server that never became ready, a masked or
72
+ * software-rendered WebGL context, a changed localStorage sentinel, or one
73
+ * or more recorded {@link ProductionRuntimeIssue}s. Callers can inspect
74
+ * `.issues` for the underlying runtime errors instead of parsing `.message`.
75
+ */
76
+ export declare class ProductionRuntimeVerificationError extends Error {
77
+ /** Runtime issues recorded before this error was thrown, if any. Empty for pure validation failures. */
78
+ readonly issues: readonly ProductionRuntimeIssue[];
79
+ constructor(message: string, issues?: readonly ProductionRuntimeIssue[], cause?: unknown);
80
+ }
81
+ /**
82
+ * Finds and process-reserves an available loopback port for an owned production
83
+ * preview. The reservation prevents parallel verifier setup in this process
84
+ * from selecting the same port; the preview must still bind with strict-port
85
+ * semantics so an external race fails closed.
86
+ */
87
+ export declare function findAvailableProductionPort(options?: AvailableProductionPortOptions): Promise<number>;
88
+ export interface WebGLRendererInfo {
89
+ /** `UNMASKED_RENDERER_WEBGL` when available, otherwise the generic (often masked) `RENDERER` parameter. */
90
+ renderer: string;
91
+ /** `UNMASKED_VENDOR_WEBGL` when available, otherwise the generic (often masked) `VENDOR` parameter. */
92
+ vendor: string;
93
+ /** True only when Chromium exposed `WEBGL_debug_renderer_info`. */
94
+ unmasked: boolean;
95
+ }
96
+ /** Reads the unmasked WebGL renderer already attached to the selected game canvas. */
97
+ export declare function readWebGLRenderer(page: Page, canvasSelector?: string): Promise<WebGLRendererInfo>;
98
+ /** Requires a real hardware-backed WebGL renderer and returns its identity. */
99
+ export declare function requireHardwareWebGL(page: Page, canvasSelector?: string): Promise<WebGLRendererInfo>;
100
+ /**
101
+ * Boots a built or deployed game in a fresh, fail-closed, silent Chromium and
102
+ * requires game-specific identity/UI proof before accepting the runtime.
103
+ */
104
+ export declare function verifyProductionRuntime(options: ProductionRuntimeOptions): Promise<ProductionRuntimeResult>;
105
+ export {};
@@ -0,0 +1,105 @@
1
+ import { type Browser, chromium, type Page } from '@playwright/test';
2
+ import { type ChromiumGpuMode } from './chromium-launch.js';
3
+ import { type OpenSilentGameOptions, type SilentQueryValue } from './playwright-config.js';
4
+ type BrowserLaunchOptions = NonNullable<Parameters<typeof chromium.launch>[0]>;
5
+ type BrowserPageOptions = Parameters<Browser['newPage']>[0];
6
+ export interface ProductionRuntimeServerOptions {
7
+ /** Executable to launch directly. Shell command strings are intentionally unsupported. */
8
+ command: string;
9
+ /** Arguments passed directly to `command`. Use the server's strict-port option. */
10
+ args?: readonly string[];
11
+ /** Child working directory. Defaults to the current process directory. */
12
+ cwd?: string;
13
+ /** Environment additions or overrides for the child process. */
14
+ env?: Readonly<NodeJS.ProcessEnv>;
15
+ /** URL polled for readiness. Defaults to the runtime `url`. */
16
+ readyUrl?: string;
17
+ /** Maximum startup time. Defaults to 15 seconds. */
18
+ startupTimeoutMs?: number;
19
+ /** Graceful shutdown time before SIGKILL. Defaults to 5 seconds. */
20
+ shutdownTimeoutMs?: number;
21
+ }
22
+ export interface ProductionRuntimeOptions {
23
+ /** Production artifact or exact-live URL to verify. */
24
+ url: string;
25
+ /** Optional owned preview server. Existing processes are never reused. */
26
+ server?: ProductionRuntimeServerOptions;
27
+ /** Chromium options. `--mute-audio` is always de-duplicated and applied last. */
28
+ browserLaunchOptions?: BrowserLaunchOptions;
29
+ /** Renderer profile. Defaults to native Chromium selection (`auto`). */
30
+ gpuMode?: ChromiumGpuMode;
31
+ /** Options for the fresh browser page. */
32
+ pageOptions?: BrowserPageOptions;
33
+ /** Extra query parameters composed with the mandatory runtime mute. */
34
+ silentParameters?: Readonly<Record<string, SilentQueryValue>>;
35
+ /** Silent marker/query overrides. The marker timeout defaults to 15 seconds here. */
36
+ silentOptions?: OpenSilentGameOptions;
37
+ /** Values written before application code and required to remain byte-for-byte unchanged. */
38
+ localStorageSentinels?: Readonly<Record<string, string | null>>;
39
+ /** Required game-specific identity plus primary UI/canvas assertions. */
40
+ assertReady: (page: Page) => Promise<void>;
41
+ /** Optional engine-specific assertion such as `window.Howler._muted === true`. */
42
+ assertSilentState?: (page: Page) => Promise<void>;
43
+ /** Brief post-ready observation window for deferred runtime errors. Defaults to 250 ms. */
44
+ settleTimeMs?: number;
45
+ }
46
+ /** Evidence returned by a successful `verifyProductionRuntime()` call. */
47
+ export interface ProductionRuntimeResult {
48
+ /** The page's URL after navigation, including the applied silent-QA query parameters. */
49
+ finalUrl: string;
50
+ /** Every requested `localStorageSentinels` key, read back after the boot completed. */
51
+ localStorage: Record<string, string | null>;
52
+ }
53
+ /**
54
+ * One runtime failure captured while the game booted: an uncaught page
55
+ * error, a `console.error` call, a network request that failed outright, or
56
+ * an HTTP response with a 4xx/5xx status. `verifyProductionRuntime()`
57
+ * accumulates these and fails closed if any were recorded.
58
+ */
59
+ export interface ProductionRuntimeIssue {
60
+ kind: 'console' | 'http' | 'pageerror' | 'requestfailed';
61
+ message: string;
62
+ /** The request/response URL, present for `'http'` and `'requestfailed'` issues. */
63
+ url?: string;
64
+ }
65
+ export interface AvailableProductionPortOptions {
66
+ /** Loopback interface used by the owned preview server. Defaults to IPv4 localhost. */
67
+ host?: string;
68
+ }
69
+ /**
70
+ * Thrown by every failure path in this module — an unreachable/occupied
71
+ * readiness URL, a server that never became ready, a masked or
72
+ * software-rendered WebGL context, a changed localStorage sentinel, or one
73
+ * or more recorded {@link ProductionRuntimeIssue}s. Callers can inspect
74
+ * `.issues` for the underlying runtime errors instead of parsing `.message`.
75
+ */
76
+ export declare class ProductionRuntimeVerificationError extends Error {
77
+ /** Runtime issues recorded before this error was thrown, if any. Empty for pure validation failures. */
78
+ readonly issues: readonly ProductionRuntimeIssue[];
79
+ constructor(message: string, issues?: readonly ProductionRuntimeIssue[], cause?: unknown);
80
+ }
81
+ /**
82
+ * Finds and process-reserves an available loopback port for an owned production
83
+ * preview. The reservation prevents parallel verifier setup in this process
84
+ * from selecting the same port; the preview must still bind with strict-port
85
+ * semantics so an external race fails closed.
86
+ */
87
+ export declare function findAvailableProductionPort(options?: AvailableProductionPortOptions): Promise<number>;
88
+ export interface WebGLRendererInfo {
89
+ /** `UNMASKED_RENDERER_WEBGL` when available, otherwise the generic (often masked) `RENDERER` parameter. */
90
+ renderer: string;
91
+ /** `UNMASKED_VENDOR_WEBGL` when available, otherwise the generic (often masked) `VENDOR` parameter. */
92
+ vendor: string;
93
+ /** True only when Chromium exposed `WEBGL_debug_renderer_info`. */
94
+ unmasked: boolean;
95
+ }
96
+ /** Reads the unmasked WebGL renderer already attached to the selected game canvas. */
97
+ export declare function readWebGLRenderer(page: Page, canvasSelector?: string): Promise<WebGLRendererInfo>;
98
+ /** Requires a real hardware-backed WebGL renderer and returns its identity. */
99
+ export declare function requireHardwareWebGL(page: Page, canvasSelector?: string): Promise<WebGLRendererInfo>;
100
+ /**
101
+ * Boots a built or deployed game in a fresh, fail-closed, silent Chromium and
102
+ * requires game-specific identity/UI proof before accepting the runtime.
103
+ */
104
+ export declare function verifyProductionRuntime(options: ProductionRuntimeOptions): Promise<ProductionRuntimeResult>;
105
+ export {};
@@ -0,0 +1,38 @@
1
+ export interface ReleaseLadderStep {
2
+ /** Label logged before/after the step runs and used to identify it in a failure result. */
3
+ name: string;
4
+ /** The step's work. A thrown error or rejected promise stops the ladder at this step. */
5
+ run: () => void | Promise<void>;
6
+ }
7
+ export interface ReleaseLadderResult {
8
+ /** True only if every step completed without throwing. */
9
+ ok: boolean;
10
+ /** Names of steps that completed successfully, in order, up to (and excluding) any failure. */
11
+ ranSteps: string[];
12
+ /** Name of the step that threw, present only when `ok` is false. */
13
+ failedStep?: string;
14
+ /** The value thrown by `failedStep`'s `run()`, present only when `ok` is false. */
15
+ error?: unknown;
16
+ }
17
+ export interface VerifyReleaseLadderOptions {
18
+ /** Called before and after each step. Defaults to `console.log` prefixed with `[verify]`. */
19
+ log?: (msg: string) => void;
20
+ /** Called on step failure, once for the failure message and again with the error's message if it's an `Error`. Defaults to `console.error` prefixed with `[verify]`. */
21
+ error?: (msg: string) => void;
22
+ }
23
+ /**
24
+ * Thin orchestrator for a `verify:*` release ladder — an ordered list of
25
+ * named steps (lint → typecheck → test → build → browser verifies →
26
+ * screenshots → native sync, or whatever a given repo's ladder is), run in
27
+ * sequence and stopped at the first failure with a labeled summary.
28
+ *
29
+ * Generalizes the reach-for-the-sky pattern of ~17 discrete
30
+ * `node scripts/verify-X.mjs` files composed via a shell `&&` chain into a
31
+ * single reusable primitive: each step is a plain function (sync or async),
32
+ * so a repo can inline its logic or delegate to existing scripts via
33
+ * `execSync`.
34
+ *
35
+ * Never throws — returns a result object so callers can decide how to
36
+ * report/exit. The CLI convention is `process.exit(result.ok ? 0 : 1)`.
37
+ */
38
+ export declare function verifyReleaseLadder(steps: ReleaseLadderStep[], options?: VerifyReleaseLadderOptions): Promise<ReleaseLadderResult>;
@@ -0,0 +1,38 @@
1
+ export interface ReleaseLadderStep {
2
+ /** Label logged before/after the step runs and used to identify it in a failure result. */
3
+ name: string;
4
+ /** The step's work. A thrown error or rejected promise stops the ladder at this step. */
5
+ run: () => void | Promise<void>;
6
+ }
7
+ export interface ReleaseLadderResult {
8
+ /** True only if every step completed without throwing. */
9
+ ok: boolean;
10
+ /** Names of steps that completed successfully, in order, up to (and excluding) any failure. */
11
+ ranSteps: string[];
12
+ /** Name of the step that threw, present only when `ok` is false. */
13
+ failedStep?: string;
14
+ /** The value thrown by `failedStep`'s `run()`, present only when `ok` is false. */
15
+ error?: unknown;
16
+ }
17
+ export interface VerifyReleaseLadderOptions {
18
+ /** Called before and after each step. Defaults to `console.log` prefixed with `[verify]`. */
19
+ log?: (msg: string) => void;
20
+ /** Called on step failure, once for the failure message and again with the error's message if it's an `Error`. Defaults to `console.error` prefixed with `[verify]`. */
21
+ error?: (msg: string) => void;
22
+ }
23
+ /**
24
+ * Thin orchestrator for a `verify:*` release ladder — an ordered list of
25
+ * named steps (lint → typecheck → test → build → browser verifies →
26
+ * screenshots → native sync, or whatever a given repo's ladder is), run in
27
+ * sequence and stopped at the first failure with a labeled summary.
28
+ *
29
+ * Generalizes the reach-for-the-sky pattern of ~17 discrete
30
+ * `node scripts/verify-X.mjs` files composed via a shell `&&` chain into a
31
+ * single reusable primitive: each step is a plain function (sync or async),
32
+ * so a repo can inline its logic or delegate to existing scripts via
33
+ * `execSync`.
34
+ *
35
+ * Never throws — returns a result object so callers can decide how to
36
+ * report/exit. The CLI convention is `process.exit(result.ok ? 0 : 1)`.
37
+ */
38
+ export declare function verifyReleaseLadder(steps: ReleaseLadderStep[], options?: VerifyReleaseLadderOptions): Promise<ReleaseLadderResult>;
@@ -0,0 +1,47 @@
1
+ export declare const SILENT_QA_QUERY_PARAMETER = "muted";
2
+ export declare const SILENT_QA_QUERY_VALUE = "1";
3
+ export declare const SILENT_QA_MARKER_ATTRIBUTE = "data-audio-mode";
4
+ export declare const SILENT_QA_MARKER_VALUE = "muted-test";
5
+ export interface SilentQaMarkerTarget {
6
+ setAttribute(name: string, value: string): void;
7
+ }
8
+ export interface ActivateSilentQaOptions {
9
+ /** Query string to inspect. Defaults to `window.location.search` in a browser. */
10
+ search?: string;
11
+ /** Runtime-only mute parameter. Defaults to `muted`. */
12
+ queryParameter?: string;
13
+ /**
14
+ * Element that owns the mute-ready marker. Defaults to `document.documentElement`.
15
+ * Pass `null` for a non-DOM runtime.
16
+ */
17
+ markerTarget?: SilentQaMarkerTarget | null;
18
+ /** Marker attribute set only after the audio engine's mute callback succeeds. */
19
+ markerAttribute?: string;
20
+ /** Marker value set only after the audio engine's mute callback succeeds. */
21
+ markerValue?: string;
22
+ }
23
+ /**
24
+ * Detects a page-lifetime mute request by parameter presence.
25
+ *
26
+ * The value is intentionally ignored so stale links such as `?muted=0` cannot
27
+ * make an agent-controlled session audible.
28
+ */
29
+ export declare function isSilentQaRequested(search?: string, queryParameter?: string): boolean;
30
+ /**
31
+ * Activates runtime-only silent QA through a consumer-owned audio mute callback.
32
+ *
33
+ * The package stays audio-engine agnostic: consumers may mute Howler, WebAudio,
34
+ * HTMLMediaElement, or another engine. The DOM marker is published only after
35
+ * the callback returns successfully, allowing browser tests to fail closed.
36
+ * This helper never reads or writes a player's persisted audio preference.
37
+ */
38
+ export declare function activateSilentQa(mute: () => void, options?: ActivateSilentQaOptions): boolean;
39
+ /**
40
+ * Asynchronous counterpart to {@link activateSilentQa}. The readiness marker
41
+ * is not published until the consumer's mute promise fulfills.
42
+ */
43
+ export declare function activateSilentQaAsync(mute: () => void | PromiseLike<void>, options?: ActivateSilentQaOptions): Promise<boolean>;
44
+ /** True after this module has successfully activated the page-lifetime override. */
45
+ export declare function isSilentQaActive(): boolean;
46
+ /** Test hook; production code must never disable a page-lifetime override. */
47
+ export declare function _resetSilentQaForTests(): void;
@@ -0,0 +1,47 @@
1
+ export declare const SILENT_QA_QUERY_PARAMETER = "muted";
2
+ export declare const SILENT_QA_QUERY_VALUE = "1";
3
+ export declare const SILENT_QA_MARKER_ATTRIBUTE = "data-audio-mode";
4
+ export declare const SILENT_QA_MARKER_VALUE = "muted-test";
5
+ export interface SilentQaMarkerTarget {
6
+ setAttribute(name: string, value: string): void;
7
+ }
8
+ export interface ActivateSilentQaOptions {
9
+ /** Query string to inspect. Defaults to `window.location.search` in a browser. */
10
+ search?: string;
11
+ /** Runtime-only mute parameter. Defaults to `muted`. */
12
+ queryParameter?: string;
13
+ /**
14
+ * Element that owns the mute-ready marker. Defaults to `document.documentElement`.
15
+ * Pass `null` for a non-DOM runtime.
16
+ */
17
+ markerTarget?: SilentQaMarkerTarget | null;
18
+ /** Marker attribute set only after the audio engine's mute callback succeeds. */
19
+ markerAttribute?: string;
20
+ /** Marker value set only after the audio engine's mute callback succeeds. */
21
+ markerValue?: string;
22
+ }
23
+ /**
24
+ * Detects a page-lifetime mute request by parameter presence.
25
+ *
26
+ * The value is intentionally ignored so stale links such as `?muted=0` cannot
27
+ * make an agent-controlled session audible.
28
+ */
29
+ export declare function isSilentQaRequested(search?: string, queryParameter?: string): boolean;
30
+ /**
31
+ * Activates runtime-only silent QA through a consumer-owned audio mute callback.
32
+ *
33
+ * The package stays audio-engine agnostic: consumers may mute Howler, WebAudio,
34
+ * HTMLMediaElement, or another engine. The DOM marker is published only after
35
+ * the callback returns successfully, allowing browser tests to fail closed.
36
+ * This helper never reads or writes a player's persisted audio preference.
37
+ */
38
+ export declare function activateSilentQa(mute: () => void, options?: ActivateSilentQaOptions): boolean;
39
+ /**
40
+ * Asynchronous counterpart to {@link activateSilentQa}. The readiness marker
41
+ * is not published until the consumer's mute promise fulfills.
42
+ */
43
+ export declare function activateSilentQaAsync(mute: () => void | PromiseLike<void>, options?: ActivateSilentQaOptions): Promise<boolean>;
44
+ /** True after this module has successfully activated the page-lifetime override. */
45
+ export declare function isSilentQaActive(): boolean;
46
+ /** Test hook; production code must never disable a page-lifetime override. */
47
+ export declare function _resetSilentQaForTests(): void;
@@ -0,0 +1,69 @@
1
+ export interface VisualBatteryCommand {
2
+ /** Executable invoked directly, without a shell. */
3
+ command: string;
4
+ /** Fixed arguments inserted before the discovered harness paths. */
5
+ args?: readonly string[];
6
+ }
7
+ export interface VisualBatteryOptions {
8
+ /** CI mode: refuses to run with a dirty baseline dir, fails on drift instead of updating. */
9
+ ci?: boolean;
10
+ /** Repo root the git commands run relative to. Defaults to `process.cwd()`. */
11
+ cwd?: string;
12
+ /**
13
+ * The `pnpm test:browser`-equivalent command, without harness paths.
14
+ * Strings are split on whitespace for backward compatibility; use the
15
+ * object form when an argument contains spaces. Commands execute directly,
16
+ * never through a shell. Defaults to `'pnpm test:browser'`.
17
+ */
18
+ testCommand?: string | VisualBatteryCommand;
19
+ /** Baseline root relative to `cwd`. Defaults to `${harnessGlob}/__screenshots__`; `baselineProfile` is appended below it. */
20
+ baselinesDir?: string;
21
+ /**
22
+ * Optional platform/profile directory below the baseline root (for example
23
+ * `linux`). The profile is also exposed to Vite browser tests as
24
+ * `VITE_VISUAL_BASELINE_PROFILE`, allowing byte-exact baselines to remain
25
+ * strict on renderers that cannot produce identical PNGs.
26
+ */
27
+ baselineProfile?: string;
28
+ /**
29
+ * Harness basenames that each need a fresh browser process. Use this for
30
+ * WebGL screenshots whose renderer state can drift after earlier canvases
31
+ * have shared a long-lived Chromium process.
32
+ */
33
+ isolatedHarnessFiles?: string[];
34
+ /** Logger, swappable for tests. Defaults to `console.log`/`console.error`. */
35
+ log?: (msg: string) => void;
36
+ error?: (msg: string) => void;
37
+ }
38
+ /**
39
+ * Thrown by every `runVisualBattery()` failure path — a missing harness dir,
40
+ * no discovered `.browser.test.ts(x)` files, a misplaced `__screenshots__`
41
+ * directory, a dirty baseline dir in `--ci` mode, a failing harness run, or
42
+ * detected drift while `ci: true`. Callers (tests, other tooling) can catch
43
+ * this specific type instead of `process.exit`, which only the CLI entry
44
+ * point (`bin/test-harness-visual-battery`) calls.
45
+ */
46
+ export declare class VisualBatteryError extends Error {
47
+ }
48
+ /**
49
+ * Deterministic git-diff-based visual-regression gate.
50
+ *
51
+ * Runs every `.browser.test.tsx` harness file under `harnessDir`, then
52
+ * diffs the resulting `__screenshots__/*.png` baselines against what's
53
+ * committed in git. No pixel-threshold fuzzing, no flaky perceptual
54
+ * comparison — a screenshot either byte-matches the committed baseline
55
+ * (via `git status --porcelain`) or it doesn't.
56
+ *
57
+ * - Update mode (`ci: false`, the default): runs the harnesses, lets new
58
+ * baselines land on disk, reports what changed so a human can review
59
+ * `git diff` and commit intentionally.
60
+ * - CI mode (`ci: true`): refuses to run at all if the baselines dir has
61
+ * uncommitted changes already (an untrusted starting state), then fails
62
+ * the process if the run produces any drift from the committed baseline.
63
+ *
64
+ * Throws `VisualBatteryError` on any failure condition instead of calling
65
+ * `process.exit` directly, so callers (tests, other tooling) can catch it;
66
+ * the CLI entry point (`bin/test-harness-visual-battery`) is the thing that
67
+ * exits the process.
68
+ */
69
+ export declare function runVisualBattery(harnessDir: string, options?: VisualBatteryOptions): void;
@@ -0,0 +1,69 @@
1
+ export interface VisualBatteryCommand {
2
+ /** Executable invoked directly, without a shell. */
3
+ command: string;
4
+ /** Fixed arguments inserted before the discovered harness paths. */
5
+ args?: readonly string[];
6
+ }
7
+ export interface VisualBatteryOptions {
8
+ /** CI mode: refuses to run with a dirty baseline dir, fails on drift instead of updating. */
9
+ ci?: boolean;
10
+ /** Repo root the git commands run relative to. Defaults to `process.cwd()`. */
11
+ cwd?: string;
12
+ /**
13
+ * The `pnpm test:browser`-equivalent command, without harness paths.
14
+ * Strings are split on whitespace for backward compatibility; use the
15
+ * object form when an argument contains spaces. Commands execute directly,
16
+ * never through a shell. Defaults to `'pnpm test:browser'`.
17
+ */
18
+ testCommand?: string | VisualBatteryCommand;
19
+ /** Baseline root relative to `cwd`. Defaults to `${harnessGlob}/__screenshots__`; `baselineProfile` is appended below it. */
20
+ baselinesDir?: string;
21
+ /**
22
+ * Optional platform/profile directory below the baseline root (for example
23
+ * `linux`). The profile is also exposed to Vite browser tests as
24
+ * `VITE_VISUAL_BASELINE_PROFILE`, allowing byte-exact baselines to remain
25
+ * strict on renderers that cannot produce identical PNGs.
26
+ */
27
+ baselineProfile?: string;
28
+ /**
29
+ * Harness basenames that each need a fresh browser process. Use this for
30
+ * WebGL screenshots whose renderer state can drift after earlier canvases
31
+ * have shared a long-lived Chromium process.
32
+ */
33
+ isolatedHarnessFiles?: string[];
34
+ /** Logger, swappable for tests. Defaults to `console.log`/`console.error`. */
35
+ log?: (msg: string) => void;
36
+ error?: (msg: string) => void;
37
+ }
38
+ /**
39
+ * Thrown by every `runVisualBattery()` failure path — a missing harness dir,
40
+ * no discovered `.browser.test.ts(x)` files, a misplaced `__screenshots__`
41
+ * directory, a dirty baseline dir in `--ci` mode, a failing harness run, or
42
+ * detected drift while `ci: true`. Callers (tests, other tooling) can catch
43
+ * this specific type instead of `process.exit`, which only the CLI entry
44
+ * point (`bin/test-harness-visual-battery`) calls.
45
+ */
46
+ export declare class VisualBatteryError extends Error {
47
+ }
48
+ /**
49
+ * Deterministic git-diff-based visual-regression gate.
50
+ *
51
+ * Runs every `.browser.test.tsx` harness file under `harnessDir`, then
52
+ * diffs the resulting `__screenshots__/*.png` baselines against what's
53
+ * committed in git. No pixel-threshold fuzzing, no flaky perceptual
54
+ * comparison — a screenshot either byte-matches the committed baseline
55
+ * (via `git status --porcelain`) or it doesn't.
56
+ *
57
+ * - Update mode (`ci: false`, the default): runs the harnesses, lets new
58
+ * baselines land on disk, reports what changed so a human can review
59
+ * `git diff` and commit intentionally.
60
+ * - CI mode (`ci: true`): refuses to run at all if the baselines dir has
61
+ * uncommitted changes already (an untrusted starting state), then fails
62
+ * the process if the run produces any drift from the committed baseline.
63
+ *
64
+ * Throws `VisualBatteryError` on any failure condition instead of calling
65
+ * `process.exit` directly, so callers (tests, other tooling) can catch it;
66
+ * the CLI entry point (`bin/test-harness-visual-battery`) is the thing that
67
+ * exits the process.
68
+ */
69
+ export declare function runVisualBattery(harnessDir: string, options?: VisualBatteryOptions): void;
package/docs/404.md ADDED
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Page not found
3
+ description: This page does not exist.
4
+ editUrl: false
5
+ head:
6
+ - tag: meta
7
+ attrs:
8
+ property: og:title
9
+ content: Page not found
10
+ tableOfContents: false
11
+ sidebar:
12
+ hidden: true
13
+ template: splash
14
+ ---
15
+
16
+ The page you're looking for doesn't exist. Head back to the
17
+ [docs home](/game-harness/) or the [getting started guide](/game-harness/getting-started/).
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: Agent development guide
3
+ description: The safe, policy-compliant workflow for trusted coding agents.
4
+ ---
5
+
6
+ Repository instructions for agents live in [AGENTS.md](https://github.com/jbcom/game-harness/blob/main/AGENTS.md). They cover the public API, silent-browser contract, package boundaries, validation, Sourcey documentation architecture, Conventional Commits, and protected merge workflow.
7
+
8
+ The repository-root `llms.txt` is a concise agent index. Sourcey generates
9
+ separate `llms.txt` and `llms-full.txt` files in the documentation build; those
10
+ describe this documentation site and are not manually maintained copies.
@@ -0,0 +1,81 @@
1
+ # Architecture
2
+
3
+ Game Harness is a small collection of composable verification primitives. It
4
+ does not own a game's tests or runtime state; it makes the boundaries around
5
+ those tests explicit and fail-closed.
6
+
7
+ ## Package boundaries
8
+
9
+ | Entry point | Responsibility | Required peer |
10
+ | --------------------- | --------------------------------------------------------------- | -------------------------------------- |
11
+ | package root | Lighthouse policy, release ladder, visual battery | None |
12
+ | `/chromium` | Renderer and mandatory mute launch profile | None |
13
+ | `/silent-qa` | Application-side runtime mute request and readiness marker | None |
14
+ | `/playwright` | Device tiers, isolated ports, preview server, silent navigation | `@playwright/test` |
15
+ | `/production-runtime` | Fresh server/browser lifecycle and runtime evidence | `@playwright/test` |
16
+ | `/vitest` | Vitest Browser Mode configuration | `vitest`, `@vitest/browser-playwright` |
17
+ | `/lighthouse` | Immutable Lighthouse CI presets | None |
18
+ | `/release-ladder` | Ordered sync/async verification steps | None |
19
+ | `/visual-battery` | Deterministic screenshot baseline orchestration | None |
20
+
21
+ The root deliberately does not re-export Playwright or Vitest integrations. A
22
+ consumer that needs only peer-free utilities therefore never loads or installs
23
+ either framework.
24
+
25
+ ## Runtime evidence flow
26
+
27
+ 1. `verifyProductionRuntime()` validates the URL, callbacks, server identity,
28
+ and lifecycle limits before starting external work.
29
+ 2. When a server is configured, its readiness URL must be unreachable first.
30
+ The command is spawned directly with an argument array and must bind with
31
+ strict-port behavior.
32
+ 3. A fresh browser launches with the selected renderer profile and exactly one
33
+ final `--mute-audio` argument.
34
+ 4. Local-storage sentinels are installed before application code.
35
+ 5. `openSilentGame()` adds the runtime-only mute query and waits for the
36
+ application readiness marker.
37
+ 6. The consumer's `assertReady` proves game identity and primary UI. An optional
38
+ `assertSilentState` proves engine-specific mute state.
39
+ 7. Console errors, page errors, failed requests, HTTP errors, and sentinel
40
+ mutations fail the verification.
41
+ 8. Browser and owned server resources are closed on success or failure.
42
+
43
+ The browser launch argument is defense in depth. The application marker is the
44
+ primary proof that its own audio graph was muted before test interaction. The
45
+ mute adapter never changes the player's persisted preference.
46
+
47
+ ## Visual baseline ownership
48
+
49
+ `runVisualBattery()` owns one canonical baseline directory. It discovers
50
+ top-level `*.browser.test.ts` and `*.browser.test.tsx` files deterministically,
51
+ runs selected harnesses in direct child processes, requires at least one PNG,
52
+ and asks Git for scoped status with argument arrays rather than shell commands.
53
+
54
+ Update mode reports reviewed changes. CI mode also refuses a dirty starting
55
+ state and fails on any resulting drift. Renderer-specific output belongs in an
56
+ explicit profile directory rather than behind a permissive pixel threshold.
57
+
58
+ ## Configuration philosophy
59
+
60
+ Factories provide production-minded defaults and validate inputs at the public
61
+ boundary. Callers can override Playwright and browser-provider details, but the
62
+ silence guard is re-applied after merges. Values that would produce ambiguous
63
+ or empty verification—invalid ports, missing device tiers, empty browser
64
+ matrices, malformed URLs, zero Lighthouse runs, or zero screenshots—fail early
65
+ with actionable errors.
66
+
67
+ ## Build and distribution
68
+
69
+ TypeScript emits independent ESM, CommonJS, and declaration trees. A nested
70
+ `dist/cjs/package.json` marks the CommonJS output without changing the package's
71
+ top-level ESM identity. Optional peers keep install boundaries narrow. Release
72
+ validation then checks:
73
+
74
+ - formatting, linting, strict types, and 100% reusable-library source coverage
75
+ (the two-line executable adapter is exercised through packed CLI smoke tests);
76
+ - both JavaScript module formats and declarations;
77
+ - package metadata and resolution through `publint` and
78
+ `@arethetypeswrong/cli`;
79
+ - clean tarball consumers for root, Playwright, production runtime, Vitest, and
80
+ CLI entry points;
81
+ - Linux, macOS, and Windows build/test portability in CI.
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Changelog
3
+ description: Release history generated and maintained by release-please.
4
+ ---
5
+
6
+ Game Harness uses release-please and Conventional Commits to create release
7
+ pull requests, versions, GitHub Releases, and this project's changelog.
8
+
9
+ Read the current [CHANGELOG.md](https://github.com/jbcom/game-harness/blob/main/CHANGELOG.md).