@isentinel/jest-roblox 0.3.7 → 0.3.9

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 CHANGED
@@ -15,10 +15,13 @@ Run your roblox-ts and Luau tests inside Roblox, get results in your terminal.
15
15
  - roblox-ts and pure Luau
16
16
  - Source-mapped errors (Luau line numbers back to `.ts` files)
17
17
  - Code coverage (via [Lute](https://github.com/luau-lang/lute) instrumentation)
18
- - Two backends: Open Cloud (remote) and Studio (local)
18
+ - Three backends: Open Cloud (remote), Studio (attached, local), and Studio CLI
19
+ (self-launched headless Studio, local)
19
20
  - Multiple output formatters (human, agent, JSON, GitHub Actions)
20
21
 
21
- > [!NOTE] roblox-ts projects currently require
22
+ <!-- prettier-ignore -->
23
+ > [!NOTE]
24
+ > roblox-ts projects currently require
22
25
  > [@isentinel/roblox-ts](https://npmx.dev/package/@isentinel/roblox-ts) for
23
26
  > source maps and coverage support.
24
27
 
@@ -68,6 +71,15 @@ Then run:
68
71
  jest-roblox
69
72
  ```
70
73
 
74
+ <!-- prettier-ignore -->
75
+ > [!NOTE]
76
+ > `projects` is optional. With it omitted, the CLI derives one project per
77
+ > `luauRoots` mount (the compiled-output dirs your Rojo project mounts,
78
+ > auto-detected from your tsconfig `outDir`), generating each project's
79
+ > `jest.config` stub for you. Set `projects` explicitly when you need
80
+ > per-project overrides (a distinct `displayName`, `setupFiles`, `include`,
81
+ > etc.) or a tighter project layout than the luau roots imply.
82
+
71
83
  ## Usage
72
84
 
73
85
  ```bash
@@ -88,6 +100,7 @@ jest-roblox --testPathPattern="modifiers|define\\.spec|triggers"
88
100
  # Use a specific backend (default "auto" picks Studio if the plugin is
89
101
  # connected, else Open Cloud if credentials are set — see Backends below)
90
102
  jest-roblox --backend studio
103
+ jest-roblox --backend studio-cli
91
104
  jest-roblox --backend open-cloud
92
105
 
93
106
  # Collect coverage
@@ -133,7 +146,7 @@ per-package declarations error loudly.
133
146
 
134
147
  | Field | What it does | Default |
135
148
  | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
136
- | `backend` | `"auto"`, `"open-cloud"`, or `"studio"` | `"auto"` |
149
+ | `backend` | `"auto"`, `"open-cloud"`, `"studio"`, or `"studio-cli"` | `"auto"` |
137
150
  | `color` | Use ANSI colors in console output | `true` |
138
151
  | `formatters` | Output formatters (`"default"`, `"agent"`, `"json"`, `"github-actions"`) | `["default"]` |
139
152
  | `gameOutput` | Write Game Output to a file — a path, or `true` for `game-output.log` under the root. In `--workspace` mode this is one grouped aggregate file across every package | — |
@@ -144,6 +157,7 @@ per-package declarations error loudly.
144
157
  | `placeId` | Open Cloud place ID | — |
145
158
  | `port` | WebSocket port for Studio backend | `3001` |
146
159
  | `silent` | Suppress console output | `false` |
160
+ | `studioPath` | Roblox Studio executable for the `studio-cli` backend (auto-detected if unset; also `--studioPath` / `JEST_ROBLOX_STUDIO_PATH`) | — |
147
161
  | `universeId` | Open Cloud universe ID | — |
148
162
 
149
163
  #### Per-package fields
@@ -169,7 +183,7 @@ Put these under `test: { ... }`.
169
183
 
170
184
  | Field | What it does | Default |
171
185
  | ------------------------ | ------------------------------------------------ | ------------------------------------ |
172
- | `projects` | Where to look for tests in the DataModel | **required** |
186
+ | `projects` | Where to look for tests in the DataModel | one project per `luauRoots` mount |
173
187
  | `testMatch` | Glob patterns that find test files | `**/*.spec.ts`, `**/*.test.ts`, etc. |
174
188
  | `testPathIgnorePatterns` | Patterns to skip | `/node_modules/`, `/dist/`, `/out/` |
175
189
  | `setupFiles` | Scripts to run before the test environment loads | — |
@@ -181,7 +195,9 @@ Put these under `test: { ... }`.
181
195
 
182
196
  Put these under `test: { ... }`.
183
197
 
184
- > [!IMPORTANT] Coverage requires [Lute](https://github.com/luau-lang/lute) to be
198
+ <!-- prettier-ignore -->
199
+ > [!IMPORTANT]
200
+ > Coverage requires [Lute](https://github.com/luau-lang/lute) to be
185
201
  > installed and on your `PATH`. Lute parses Luau ASTs so the CLI can insert
186
202
  > coverage probes.
187
203
 
@@ -194,7 +210,9 @@ Put these under `test: { ... }`.
194
210
  | `coveragePathIgnorePatterns` | Files to leave out of coverage | test files, `node_modules`, `rbxts_include` |
195
211
  | `collectCoverageFrom` | Globs for files to include in coverage | — |
196
212
 
197
- > [!NOTE] Coverage uses vitest `all` / Istanbul semantics: every instrumented
213
+ <!-- prettier-ignore -->
214
+ > [!NOTE]
215
+ > Coverage uses vitest `all` / Istanbul semantics: every instrumented
198
216
  > file matching the include globs is reported, so a source file with no test
199
217
  > shows **0%** (and fails `coverageThreshold`) instead of being silently
200
218
  > omitted. When `collectCoverageFrom` is unset for a multi-project run, the
@@ -258,7 +276,7 @@ export default defineConfig({
258
276
 
259
277
  ## Backends
260
278
 
261
- Two ways to run tests, plus an auto-pick:
279
+ Three ways to run tests, plus an auto-pick:
262
280
 
263
281
  ### Auto (default)
264
282
 
@@ -320,7 +338,9 @@ Connects to Roblox Studio over WebSocket. Faster than Open Cloud (no upload
320
338
  step), but Studio must be open with the plugin running. Studio doesn't expose
321
339
  which place is open, so multiple concurrent projects aren't supported yet.
322
340
 
323
- > [!NOTE] For `--coverage`, prefer `--backend open-cloud` since the coverage
341
+ <!-- prettier-ignore -->
342
+ > [!NOTE]
343
+ > For `--coverage`, prefer `--backend open-cloud` since the coverage
324
344
  > output is built to a separate output under `.jest-roblox/coverage/` that is
325
345
  > likely not the studio place being served.
326
346
 
@@ -342,12 +362,54 @@ Or download `JestRobloxRunner.rbxm` from the
342
362
  [latest release](https://github.com/christopher-buss/jest-roblox-cli/releases)
343
363
  and drop it into your Studio plugins folder.
344
364
 
365
+ ### Studio CLI (self-launched, local)
366
+
367
+ `--backend studio-cli` owns the whole Studio lifecycle: it builds its own place,
368
+ launches Roblox Studio headless via Studio's `--task RunScript` interface,
369
+ drives the installed plugin's Run mode, reads the result from Studio's output
370
+ log, and quits Studio. No API key, no upload, no pre-opened editor — you just
371
+ need Studio installed (logged in) with the jest plugin. It spawns its own
372
+ isolated Studio instance, so any editor you already have open is untouched.
373
+
374
+ It is selected only when you ask for it explicitly — `auto` never launches a
375
+ Studio process on its own. Studio is auto-discovered per-OS; override the
376
+ executable with `studioPath` (config key), `--studioPath`, or
377
+ `JEST_ROBLOX_STUDIO_PATH`. The backend is serial: `--parallel > 1` errors.
378
+
379
+ Pass `--headed` to show the Studio window during the run instead of the default
380
+ hidden one — useful for watching a slow run or a hang (Studio still self-quits
381
+ when tests finish, so a fast run just flashes). It is a per-run debugging flag,
382
+ CLI-only and inert on every other backend.
383
+
384
+ Once the result lands, Studio is shut down gracefully: it is allowed to close
385
+ the place — running any edit-mode plugin `BindToClose` handlers and freeing the
386
+ place lock — and is then killed the instant the lock releases, skipping Studio's
387
+ slow telemetry teardown. The shutdown is decoupled from the result, so pass/fail
388
+ prints immediately and the process exits once teardown finishes.
389
+
390
+ Unlike the attached `studio` backend, `--coverage` works here: studio-cli opens
391
+ the Coverage-Instrumented Place instead of the Clean Place, so the report
392
+ universe, thresholds, reporters, and exclusions behave identically to the
393
+ open-cloud backend (including the all-files semantics where an untested included
394
+ file reports **0%** and fails `coverageThreshold`).
395
+
396
+ <!-- prettier-ignore -->
397
+ > [!NOTE]
398
+ > studio-cli is a local-developer convenience backend (it needs a logged-in
399
+ > Studio and the installed plugin), not a CI path.
400
+
345
401
  ## Workspace mode
346
402
 
347
403
  Run tests across multiple packages in a pnpm workspace in a single invocation.
348
- Open Cloud only Studio backend is not supported.
349
-
350
- > [!NOTE] Package discovery uses one of two sources. By default it reads
404
+ Works on every backend: Open Cloud (fans packages across parallel tasks),
405
+ `studio-cli` (one self-launched Studio process drives every package, no
406
+ sharding), and the attached `studio` backend (runs the workspace inside an open
407
+ Studio — handy for debugging the flow). `studio-cli` is serial, so
408
+ `--parallel > 1` is rejected with `--workspace`.
409
+
410
+ <!-- prettier-ignore -->
411
+ > [!NOTE]
412
+ > Package discovery uses one of two sources. By default it reads
351
413
  > `pnpm-workspace.yaml` at the workspace root. Alternatively, declare a
352
414
  > `workspace` block in your jest config (see
353
415
  > [Workspaces without pnpm](#workspaces-without-pnpm)) to enumerate packages by
@@ -440,8 +502,10 @@ project) under `.jest-roblox/output/`.
440
502
 
441
503
  | Flag | What it does |
442
504
  | -------------------------------- | ----------------------------------------------------------------------------------------------------------- |
443
- | `--backend <type>` | Choose `auto`, `open-cloud`, or `studio` |
505
+ | `--backend <type>` | Choose `auto`, `open-cloud`, `studio`, or `studio-cli` |
444
506
  | `--port <n>` | WebSocket port for Studio |
507
+ | `--studioPath <path>` | Roblox Studio executable for `studio-cli` (auto-detected if unset) |
508
+ | `--headed` | Show the Studio window during the run (`studio-cli` only; default: hidden) |
445
509
  | `--config <path>` | Path to config file |
446
510
  | `--testPathPattern <regex>` | Filter test files by path |
447
511
  | `-t, --testNamePattern <regex>` | Filter tests by name |
@@ -486,7 +550,9 @@ project) under `.jest-roblox/output/`.
486
550
  5. Maps Luau line numbers to TypeScript via source maps (roblox-ts only)
487
551
  6. Prints results
488
552
 
489
- > [!NOTE] Coverage adds extra steps: copy Luau files, insert tracking probes,
553
+ <!-- prettier-ignore -->
554
+ > [!NOTE]
555
+ > Coverage adds extra steps: copy Luau files, insert tracking probes,
490
556
  > build a separate place file, then map hit counts back to source. For
491
557
  > roblox-ts, this goes through source maps to report TypeScript lines.
492
558
 
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from "node:fs";
3
+ import { registerHooks } from "node:module";
3
4
  import { dirname, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
7
+ import { load, resolve as resolveLuau } from "../loaders/luau-raw.mjs";
8
+
6
9
  const sourceEntry = resolve(dirname(fileURLToPath(import.meta.url)), "../src/cli.ts");
7
10
 
8
- const { register } = await import("node:module");
9
- register("../loaders/luau-raw.mjs", import.meta.url);
11
+ registerHooks({ load, resolve: resolveLuau });
10
12
 
11
- if (existsSync(sourceEntry)) {
12
- const { main } = await import("../src/cli.ts");
13
- await main();
14
- } else {
15
- const { main } = await import("../dist/cli.mjs");
16
- await main();
17
- }
13
+ const { main } = existsSync(sourceEntry)
14
+ ? await import("../src/cli.ts")
15
+ : await import("../dist/cli.mjs");
16
+ await main();
package/dist/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as CliOptions } from "./schema-CCF2E3k_.mjs";
1
+ import { t as CliOptions } from "./schema-i-pZhLCV.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare function parseArgs(args: Array<string>): CliOptions;
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as loadConfig, D as outputMultiResult, H as formatBanner, O as outputSingleResult, P as parseGameOutput, Q as mergeCliWithConfig, Y as LuauScriptError, dt as version, f as formatMissingScopes, lt as isValidBackend, n as runJestRoblox, ot as VALID_BACKENDS, p as walkErrorChain, ut as ConfigError } from "./run-Bh8G33J1.mjs";
1
+ import { B as parseGameOutput, J as formatBanner, N as outputMultiResult, P as outputSingleResult, bt as version, ct as loadConfig, ht as VALID_BACKENDS, n as runJestRoblox, st as mergeCliWithConfig, tt as LuauScriptError, v as formatMissingScopes, vt as isValidBackend, y as walkErrorChain, yt as ConfigError } from "./run-BM5sqclu.mjs";
2
2
  import { OpenCloudError } from "@bedrock-rbx/ocale";
3
3
  import process from "node:process";
4
4
  import { parseArgs as parseArgs$1 } from "node:util";
@@ -9,8 +9,13 @@ const HELP_TEXT = `
9
9
  Usage: jest-roblox [options] [files...]
10
10
 
11
11
  Options:
12
- --backend <type> Backend: "auto", "open-cloud", or "studio" (default: auto)
12
+ --backend <type> Backend: "auto", "open-cloud", "studio", or
13
+ "studio-cli" (default: auto)
13
14
  --port <number> WebSocket port for studio backend (default: 3001)
15
+ --studioPath <path> Roblox Studio executable for the studio-cli
16
+ backend (auto-detected if not set)
17
+ --headed Show the Studio window during the run
18
+ (studio-cli backend only; default: hidden)
14
19
  --config <path> Path to config file
15
20
  --testPathPattern <regex> Filter test files by path pattern
16
21
  -t, --testNamePattern <regex> Filter tests by name pattern
@@ -96,6 +101,7 @@ function parseArgs(args) {
96
101
  type: "string"
97
102
  },
98
103
  "gameOutput": { type: "string" },
104
+ "headed": { type: "boolean" },
99
105
  "help": {
100
106
  default: false,
101
107
  type: "boolean"
@@ -126,6 +132,7 @@ function parseArgs(args) {
126
132
  "showLuau": { type: "boolean" },
127
133
  "silent": { type: "boolean" },
128
134
  "sourceMap": { type: "boolean" },
135
+ "studioPath": { type: "string" },
129
136
  "testNamePattern": {
130
137
  short: "t",
131
138
  type: "string"
@@ -166,6 +173,7 @@ function parseArgs(args) {
166
173
  files: positionals.length > 0 ? positionals : void 0,
167
174
  formatters: values.formatters,
168
175
  gameOutput: values.gameOutput,
176
+ headed: values.headed,
169
177
  help: values.help,
170
178
  outputFile: values.outputFile,
171
179
  packages: values.packages,
@@ -180,6 +188,7 @@ function parseArgs(args) {
180
188
  showLuau: values["no-show-luau"] === true ? false : values.showLuau,
181
189
  silent: values.silent,
182
190
  sourceMap: values.sourceMap,
191
+ studioPath: values.studioPath,
183
192
  testNamePattern: values.testNamePattern,
184
193
  testPathPattern: values.testPathPattern,
185
194
  timeout,
@@ -206,6 +215,7 @@ async function main() {
206
215
  process.exitCode = await run(process.argv.slice(2));
207
216
  }
208
217
  const PARALLEL_FLAG = "--parallel";
218
+ const IntegerLikePattern = /^-?\d+$/;
209
219
  function normalizeParallelFlag(args) {
210
220
  const out = [];
211
221
  for (let index = 0; index < args.length; index++) {
@@ -215,7 +225,7 @@ function normalizeParallelFlag(args) {
215
225
  continue;
216
226
  }
217
227
  const next = args[index + 1];
218
- if (next !== void 0 && !next.startsWith("-") && (next === "auto" || /^-?\d+$/.test(next))) {
228
+ if (next !== void 0 && !next.startsWith("-") && (next === "auto" || IntegerLikePattern.test(next))) {
219
229
  out.push(PARALLEL_FLAG, next);
220
230
  index += 1;
221
231
  } else out.push(PARALLEL_FLAG, "auto");
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as SnapshotFormatOptions, a as DisplayName, b as defineProject, c as GlobalTestConfig, d as ProjectEntry, f as ProjectTestConfig, g as SharedTestConfig, h as SHARED_TEST_KEYS, i as DEFAULT_CONFIG, l as InlineProjectConfig, m as ResolvedConfig, n as Config, o as FormatterEntry, p as ROOT_CLI_KEYS, r as ConfigInput, s as GLOBAL_TEST_KEYS, t as CliOptions, u as JEST_ARGV_EXCLUDED_KEYS, v as WorkspaceConfig, x as TypecheckConfig, y as defineConfig } from "./schema-CCF2E3k_.mjs";
1
+ import { _ as SnapshotFormatOptions, a as DisplayName, b as defineProject, c as GlobalTestConfig, d as ProjectEntry, f as ProjectTestConfig, g as SharedTestConfig, h as SHARED_TEST_KEYS, i as DEFAULT_CONFIG, l as InlineProjectConfig, m as ResolvedConfig, n as Config, o as FormatterEntry, p as ROOT_CLI_KEYS, r as ConfigInput, s as GLOBAL_TEST_KEYS, t as CliOptions, u as JEST_ARGV_EXCLUDED_KEYS, v as WorkspaceConfig, x as TypecheckConfig, y as defineConfig } from "./schema-i-pZhLCV.mjs";
2
2
  import { type } from "arktype";
3
3
  import { StorageClient } from "@bedrock-rbx/ocale/storage";
4
4
  import { WebSocket, WebSocketServer } from "ws";
@@ -106,6 +106,44 @@ interface CoverageArtifacts {
106
106
  }
107
107
  declare function readBuildManifest(filePath: string, options?: ReadBuildManifestOptions): ReadBuildManifestResult;
108
108
  //#endregion
109
+ //#region src/artifacts/build-coverage-place.d.ts
110
+ /**
111
+ * Everything a caller (a Node-only "Machine A") needs after producing the
112
+ * coverage-instrumented place offline, without executing any suite: the built
113
+ * place (`coveragePlace.path` + content hash) and the paths of the sibling
114
+ * manifests it shares a `buildId` with. The place is left on disk for the caller
115
+ * to copy to the run machine — it is never cleaned.
116
+ */
117
+ interface CoveragePlaceBundle {
118
+ buildId: string;
119
+ /** The always-on build record's path (`build-manifest.json`). */
120
+ buildManifestPath: string;
121
+ /** The coverage-data sibling manifest's path (`coverage-manifest.json`). */
122
+ coverageManifestPath: string;
123
+ /** The instrumented place: cwd-relative path + SHA-256 of its bytes. */
124
+ coveragePlace: BuildManifestArtifact;
125
+ /** Per-project DataModel paths baked into the place. */
126
+ projects: Array<BuildManifestProject>;
127
+ /** `false` on the incremental no-change reuse path (place was not rebuilt). */
128
+ rebuilt: boolean;
129
+ }
130
+ /**
131
+ * Build the coverage-instrumented place **without running it** — the offline
132
+ * half of the split producer. Instruments (incrementally) and rojo-builds the
133
+ * place, bakes each project's `jest.config` stub into it so any runner that
134
+ * opens the place can discover and run the suite unaided, publishes the sibling
135
+ * Build + Coverage manifests, and hands back the place. No backend is resolved,
136
+ * no suite executes, nothing hits the network. Coverage collection is forced on
137
+ * regardless of the input config.
138
+ *
139
+ * The counterpart to `prepareArtifacts`, minus the run and the Clean Place —
140
+ * the entry point for a machine that cannot execute Roblox at all. It shares the
141
+ * `prepareBakedCoverage` seam with the run path but always bakes stubs (the run
142
+ * path skips baking for studio-cli, which injects configs at runtime), because a
143
+ * place handed to a foreign runner must be self-contained.
144
+ */
145
+ declare function buildCoveragePlace(config: ResolvedConfig): Promise<CoveragePlaceBundle>;
146
+ //#endregion
109
147
  //#region src/coverage-pipeline/types.d.ts
110
148
  /**
111
149
  * Raw hit counts for a single file, keyed by statement/function index.
@@ -352,7 +390,16 @@ interface Backend {
352
390
  readonly kind: BackendKind;
353
391
  runTests(options: BackendOptions): Promise<BackendResult>;
354
392
  }
355
- type BackendKind = "open-cloud" | "studio";
393
+ type BackendKind = "open-cloud" | "studio" | "studio-cli";
394
+ /**
395
+ * Whether this is a workspace (multi-package) run. Workspace jobs each carry
396
+ * their owning package name (`pkg`); single-/multi-project jobs never do, and
397
+ * the run layer builds them all-or-none — so any job with `pkg` means the whole
398
+ * run is a workspace run. The Studio backends key off this to drive the plugin's
399
+ * staged-materializer dispatch (`workspace.entries`) instead of the configs
400
+ * path. `buildWorkspaceEntries` then fails fast if a job is missing `pkg`, so a
401
+ * malformed (mixed) array surfaces as a clear error rather than a bad payload.
402
+ */
356
403
  //#endregion
357
404
  //#region packages/roblox-runner/dist/index.d.mts
358
405
  //#endregion
@@ -423,6 +470,154 @@ declare class OpenCloudBackend implements Backend {
423
470
  }
424
471
  declare function createOpenCloudBackend(credentials: OpenCloudCredentials): OpenCloudBackend;
425
472
  //#endregion
473
+ //#region src/staging/synthesizer.d.ts
474
+ interface StubMount {
475
+ absStubPath: string;
476
+ dataModelPath: string;
477
+ }
478
+ interface CoverageRoot {
479
+ /** Path relative to `packageDirectory` that points at the original luau root. */
480
+ luauRoot: string;
481
+ /** Absolute path to the instrumented shadow directory for the same root. */
482
+ shadowDir: string;
483
+ }
484
+ interface PackageDescriptor {
485
+ name: string;
486
+ /**
487
+ * Per-package `coverageCache` opt-out, forwarded to
488
+ * `prepareWorkspaceCoverage` so the cache gate honors each package's own
489
+ * declaration. Not read by synthesis itself.
490
+ */
491
+ coverageCache?: boolean;
492
+ /**
493
+ * Per-package coverage ignore patterns, forwarded to
494
+ * `prepareWorkspaceCoverage` so the matcher reflects the merged pkgConfig
495
+ * instead of the workspace-root default. Not read by synthesis itself.
496
+ */
497
+ coveragePathIgnorePatterns?: Array<string>;
498
+ /**
499
+ * When set, $path entries that fall inside any listed `luauRoot` are
500
+ * redirected to the corresponding `shadowDir` so the synthesized place
501
+ * picks up instrumented sources for this package only. Packages without
502
+ * `coverageRoots` use their original $path entries unchanged.
503
+ */
504
+ coverageRoots?: Array<CoverageRoot>;
505
+ /**
506
+ * Per-package `luauRoots`, forwarded to `prepareWorkspaceCoverage` so the
507
+ * short-circuit honors the user-listed source dirs. Not read by synthesis
508
+ * itself.
509
+ */
510
+ luauRoots?: Array<string>;
511
+ packageDirectory: string;
512
+ rojoProjectPath: string;
513
+ stubMounts?: Array<StubMount>;
514
+ }
515
+ //#endregion
516
+ //#region src/staging/place-builder.d.ts
517
+ interface BuildPlaceOptions {
518
+ /**
519
+ * Force `ServerScriptService.LoadStringEnabled = true` on the built place.
520
+ * Used by studio-cli's Clean Place, whose Run-mode runner gates on
521
+ * LoadString. Forwarded verbatim to {@link synthesize}.
522
+ */
523
+ loadStringEnabled?: boolean;
524
+ packages: Array<PackageDescriptor>;
525
+ placeFile: string;
526
+ projectFile: string;
527
+ wrap?: boolean;
528
+ }
529
+ //#endregion
530
+ //#region src/backends/studio-cli.d.ts
531
+ interface StudioCliLaunchRequest {
532
+ /** Full Studio CLI argument vector (already absolute paths). */
533
+ args: Array<string>;
534
+ /**
535
+ * Show the Studio window during the run (`--headed`) instead of the default
536
+ * hidden window. Maps to `windowsHide: !headed` in {@link spawnStudio}.
537
+ */
538
+ headed: boolean;
539
+ /**
540
+ * Absolute path of the place Studio opens. Used only to clear a stale
541
+ * `<place>.lock` a previously killed Studio could not remove itself.
542
+ */
543
+ placeFile: string;
544
+ /** Absolute path to the Studio executable. */
545
+ studioPath: string;
546
+ }
547
+ /**
548
+ * A launched Studio the host can kill once the result arrives (or on timeout).
549
+ * The injected seam: unit tests return a fake that drives a canned result frame
550
+ * over the mock WebSocket server instead of launching Studio.
551
+ */
552
+ interface StudioCliProcess {
553
+ /**
554
+ * Terminate Studio immediately (`TerminateProcess`), skipping graceful
555
+ * shutdown. Used on every hung/error/timeout path.
556
+ */
557
+ kill: () => void;
558
+ /**
559
+ * Graceful teardown: wait for Studio to release
560
+ * `<place>.lock` — which happens only after every edit-mode `BindToClose`
561
+ * handler ran and `ClosePlace` finished — then terminate it, skipping
562
+ * Studio's ~30s post-close telemetry drain. Hard-kills anyway after
563
+ * `graceCapMs` (a long-yielding handler). Returns immediately; the watch runs
564
+ * in the background, keeping node's event loop alive (the child handle + a
565
+ * poll timer) until it fires, so the CLI prints results now and the process
566
+ * exits once teardown completes. The caller closes the result server first,
567
+ * which is what lets the bootstrap return and `--quitAfterExecution` begin
568
+ * the graceful close.
569
+ */
570
+ killOnLockRelease: (graceCapMs: number) => void;
571
+ /** Subscribe to a spawn failure (e.g. a bad `studioPath`). */
572
+ onError: (listener: (error: Error) => void) => void;
573
+ }
574
+ /** Spawns Studio and returns the handle the host kills. */
575
+ type StudioCliLauncher = (request: StudioCliLaunchRequest) => StudioCliProcess;
576
+ interface StudioCliOptions {
577
+ /** Place Builder seam; defaults to the real {@link defaultBuildPlace}. */
578
+ buildPlace?: (options: BuildPlaceOptions) => BuildManifestArtifact;
579
+ /** Result-server factory seam; defaults to an ephemeral-port `ws` server. */
580
+ createServer?: () => WebSocketServer;
581
+ /** Studio-executable resolver seam; defaults to {@link discoverStudioPath}. */
582
+ discover?: (override: string | undefined) => string;
583
+ /**
584
+ * Backstop for the graceful teardown: hard-kill if `<place>.lock` isn't
585
+ * released within this many ms. Defaults to {@link GRACEFUL_SHUTDOWN_CAP_MS}.
586
+ */
587
+ gracefulShutdownTimeout?: number;
588
+ /**
589
+ * Show the Studio window during the run (`--headed`). CLI-only — never read
590
+ * from config. Defaults to false (hidden window).
591
+ */
592
+ headed?: boolean;
593
+ /** Process launcher seam; defaults to the real {@link spawnStudio}. */
594
+ launch?: StudioCliLauncher;
595
+ /** Explicit Studio executable path (override from config / CLI / env). */
596
+ studioPath?: string;
597
+ /** Run timeout in milliseconds. Defaults to 300000. */
598
+ timeout?: number;
599
+ }
600
+ declare class StudioCliBackend implements Backend {
601
+ private readonly buildPlace;
602
+ private readonly createServer;
603
+ private readonly discover;
604
+ private readonly gracefulShutdownTimeout;
605
+ private readonly headed;
606
+ private readonly launch;
607
+ private readonly studioPath?;
608
+ private readonly timeout;
609
+ readonly kind = "studio-cli";
610
+ constructor(options?: StudioCliOptions);
611
+ runTests(options: BackendOptions): Promise<BackendResult>;
612
+ /**
613
+ * Build the Clean Place for a normal (non-coverage) run and return its path.
614
+ * `loadStringEnabled` is forced on so the Run-mode runner's LoadString gate
615
+ * passes. Coverage runs skip this and open the instrumented place instead.
616
+ */
617
+ private buildCleanPlace;
618
+ }
619
+ declare function createStudioCliBackend(options?: StudioCliOptions): StudioCliBackend;
620
+ //#endregion
426
621
  //#region src/backends/studio.d.ts
427
622
  interface PreConnected {
428
623
  server: WebSocketServer;
@@ -604,6 +799,40 @@ type ReadManifestResult = ParsedManifest<CoverageManifest>;
604
799
  declare const manifestSchema: type<CoverageManifest>;
605
800
  declare function readManifest(filePath: string): ReadManifestResult;
606
801
  //#endregion
802
+ //#region src/coverage-pipeline/merge-raw-coverage.d.ts
803
+ /**
804
+ * Additively merge two raw coverage datasets. Overlapping files have their
805
+ * hit counts summed (matching istanbul-lib-coverage's semantics).
806
+ */
807
+ declare function mergeRawCoverage(target: RawCoverageData | undefined, source: RawCoverageData | undefined): RawCoverageData | undefined;
808
+ //#endregion
809
+ //#region src/coverage-pipeline/raw-coverage.d.ts
810
+ /**
811
+ * Normalize a raw coverage table into typed {@link RawCoverageData}. The input
812
+ * is the per-file hit table the coverage probes accumulate at runtime — the
813
+ * `_G.__jest_roblox_cov` global, or the `_coverage` field of a run envelope —
814
+ * keyed by the stable per-file join key (`fileKey`). Luau serializes the
815
+ * `s`/`f` counters as 1-based arrays and `b` as an array of arrays; this
816
+ * canonicalizes them to string-keyed records while leaving the fileKey verbatim
817
+ * (it is the byte-identical join key the static maps are also keyed to). Returns
818
+ * `undefined` when the input is not an object or carries no file with a
819
+ * statement map.
820
+ */
821
+ declare function normalizeRawCoverage(coverage: unknown): RawCoverageData | undefined;
822
+ /**
823
+ * Extract raw coverage from a completed run's result envelope — the companion
824
+ * seam for a run this CLI did not launch. Accepts the plugin's `jestOutput`
825
+ * (a JSON string or an already-parsed object), or the bare `_G.__jest_roblox_cov`
826
+ * table read straight off the run. When an object carries a `_coverage` field it
827
+ * is used; otherwise the object is treated as the hit table itself. Returns
828
+ * `undefined` for malformed JSON or an envelope with no coverage.
829
+ *
830
+ * A multi-project result (`{ entries: [{ jestOutput }, …] }`) carries one
831
+ * envelope per project; parse each `entries[i].jestOutput` and combine with
832
+ * `mergeRawCoverage`.
833
+ */
834
+ declare function parseCoverageEnvelope(output: unknown): RawCoverageData | undefined;
835
+ //#endregion
607
836
  //#region src/coverage-pipeline/attribution.d.ts
608
837
  interface AttributionResult {
609
838
  /** Per Luau file: statement id → ids of the tests that covered it. */
@@ -942,6 +1171,16 @@ interface MappedCoverageResult {
942
1171
  files: Record<string, MappedFileCoverage>;
943
1172
  }
944
1173
  //#endregion
1174
+ //#region src/coverage-pipeline/agent-table-filter.d.ts
1175
+ /**
1176
+ * Decides whether a single coverage-universe source file is shown in the agent
1177
+ * **text table**. Receives a normalized-absolute source path (see
1178
+ * {@link narrowMappedForAgentTable}). This is a display-only narrowing layered
1179
+ * on top of `filterCoverageUniverse`: thresholds, the totals line, and the
1180
+ * lcov/html/json artifacts always keep the full universe.
1181
+ */
1182
+ type CoverageDisplayPredicate = (normalizedAbsolutePath: string) => boolean;
1183
+ //#endregion
945
1184
  //#region src/run/types.d.ts
946
1185
  type RunMode = "multi" | "single" | "workspace";
947
1186
  interface ProjectResult {
@@ -961,6 +1200,13 @@ interface SingleRunResult {
961
1200
  * place set it has.
962
1201
  */
963
1202
  coverageArtifacts?: CoverageArtifacts;
1203
+ /**
1204
+ * Narrows the agent coverage **text table** to the directly-filtered source
1205
+ * files on a filtered run (single file / `--testPathPattern`). Display-only:
1206
+ * thresholds, totals, and lcov/html/json keep the full universe. Undefined on
1207
+ * a full run.
1208
+ */
1209
+ coverageDisplayFilter?: CoverageDisplayPredicate;
964
1210
  mode: "single";
965
1211
  preCoverageMs: number;
966
1212
  runtimeResult?: ExecuteResult;
@@ -971,6 +1217,13 @@ interface MultiRunResult {
971
1217
  collectCoverageFrom?: Array<string>;
972
1218
  /** Producer record for the entry point to emit a Build Manifest from. */
973
1219
  coverageArtifacts?: CoverageArtifacts;
1220
+ /**
1221
+ * Narrows the agent coverage **text table** to the directly-filtered source
1222
+ * files on a filtered run (positional files / `--testPathPattern`, or the
1223
+ * selected `--project` scope). Display-only: thresholds, totals, and
1224
+ * lcov/html/json keep the full universe. Undefined on a full run.
1225
+ */
1226
+ coverageDisplayFilter?: CoverageDisplayPredicate;
974
1227
  merged: MultiProjectMerged;
975
1228
  mode: "multi";
976
1229
  preCoverageMs: number;
@@ -1427,4 +1680,4 @@ declare function visitExpression(expression: AstExpr, visitor: LuauVisitor): voi
1427
1680
  declare function visitStatement(statement: AstStat, visitor: LuauVisitor): void;
1428
1681
  declare function visitBlock(block: AstStatBlock, visitor: LuauVisitor): void; //#endregion
1429
1682
  //#endregion
1430
- export { type ArtifactBundle, type AstExpr, type AstExprBinary, type AstExprCall, type AstExprFunction, type AstStat, type AstStatBlock, BUILD_MANIFEST_VERSION, type Backend, type BackendOptions, type BuildManifest, type BuildManifestArtifact, type BuildManifestFileRecord, type BuildManifestProject, type CliOptions, type Config, type ConfigInput, type CoverageManifest, DEFAULT_CONFIG, type DisplayName, type ExecuteResult, type FormatOutputOptions, type FormatterEntry, GLOBAL_TEST_KEYS, type GameOutputEntry, type GitHubActionsFormatterOptions, type GlobalTestConfig, type InlineProjectConfig, type InstrumentedFileRecord, JEST_ARGV_EXCLUDED_KEYS, type JestArgv, type JestResult, type LuauSpan, type LuauVisitor, MANIFEST_VERSION, type MultiProjectMerged, type MultiRunResult, type NonInstrumentedFileRecord, OpenCloudBackend, type ProjectEntry, type ProjectInput, type ProjectResult, type ProjectTestConfig, ROOT_CLI_KEYS, type ReadBuildManifestOptions, type ReadBuildManifestResult, type ReadManifestResult as ReadCoverageManifestResult, type ResolvedConfig, type ResolvedProjectConfig, type RunMode, type RunOptions, type RunProjectsOptions, type RunProjectsResult, type RunResult, SHARED_TEST_KEYS, type SharedTestConfig, type SingleRunResult, StudioBackend, type TestCaseResult, type TestDefinition, type TestFileResult, type TestRecord, type TestStatus, type TscErrorInfo, type TypecheckOptions, type WorkspaceConfig, type WorkspaceRunResult, buildJestArgv, buildManifestSchema, manifestSchema as coverageManifestSchema, createOpenCloudBackend, createStudioBackend, defineConfig, defineProject, extractJsonFromOutput, formatAnnotations, formatExecuteOutput, formatFailure, formatGameOutputNotice, formatJobSummary, formatJson, formatResult, formatTestSummary, generateTestScript, hashFile, loadConfig, parseGameOutput, parseJestOutput, prepareArtifacts, readBuildManifest, readManifest as readCoverageManifest, resolveConfig, runJestRoblox, runProjects, runTypecheck, visitBlock, visitExpression, visitStatement, writeGameOutput, writeJsonFile };
1683
+ export { type ArtifactBundle, type AstExpr, type AstExprBinary, type AstExprCall, type AstExprFunction, type AstStat, type AstStatBlock, BUILD_MANIFEST_VERSION, type Backend, type BackendOptions, type BuildManifest, type BuildManifestArtifact, type BuildManifestFileRecord, type BuildManifestProject, type CliOptions, type Config, type ConfigInput, type CoverageManifest, type CoveragePlaceBundle, DEFAULT_CONFIG, type DisplayName, type ExecuteResult, type FormatOutputOptions, type FormatterEntry, GLOBAL_TEST_KEYS, type GameOutputEntry, type GitHubActionsFormatterOptions, type GlobalTestConfig, type InlineProjectConfig, type InstrumentedFileRecord, JEST_ARGV_EXCLUDED_KEYS, type JestArgv, type JestResult, type LuauSpan, type LuauVisitor, MANIFEST_VERSION, type MultiProjectMerged, type MultiRunResult, type NonInstrumentedFileRecord, OpenCloudBackend, type ProjectEntry, type ProjectInput, type ProjectResult, type ProjectTestConfig, ROOT_CLI_KEYS, type RawCoverageData, type RawFileCoverage, type ReadBuildManifestOptions, type ReadBuildManifestResult, type ReadManifestResult as ReadCoverageManifestResult, type ResolvedConfig, type ResolvedProjectConfig, type RunMode, type RunOptions, type RunProjectsOptions, type RunProjectsResult, type RunResult, SHARED_TEST_KEYS, type SharedTestConfig, type SingleRunResult, StudioBackend, StudioCliBackend, type TestCaseResult, type TestDefinition, type TestFileResult, type TestRecord, type TestStatus, type TscErrorInfo, type TypecheckOptions, type WorkspaceConfig, type WorkspaceRunResult, buildCoveragePlace, buildJestArgv, buildManifestSchema, manifestSchema as coverageManifestSchema, createOpenCloudBackend, createStudioBackend, createStudioCliBackend, defineConfig, defineProject, extractJsonFromOutput, formatAnnotations, formatExecuteOutput, formatFailure, formatGameOutputNotice, formatJobSummary, formatJson, formatResult, formatTestSummary, generateTestScript, hashFile, loadConfig, mergeRawCoverage, normalizeRawCoverage, parseCoverageEnvelope, parseGameOutput, parseJestOutput, prepareArtifacts, readBuildManifest, readManifest as readCoverageManifest, resolveConfig, runJestRoblox, runProjects, runTypecheck, visitBlock, visitExpression, visitStatement, writeGameOutput, writeJsonFile };