# Playtesting implementation progress

The v3 scenario runner and profiler integration remain in development. A managed-session MCP preview now provides nonblocking launch, status, context inspection, and server-context stop requests, alongside the input and observation improvements below. Live Studio lifecycle verification remains outstanding.

## Managed session preview

Use `studio_test_session` with an explicit `connectionId`. `action: "context"` reports edit/server/client role and a managed run ID when available. Start from an edit connection with `action: "start"`, `mode` (run/play/multiplayer), `players`, `timeoutMs`, and optional `args`. Retain the returned run ID and originating connection for `action: "status"`. The operation returns immediately; `executing` means the engine invocation is pending, not that gameplay is ready. This preview always reports `ready: false`; check scenario-specific state before interaction.

Managed tests wrap game arguments as `{ args: <your args>, __dominusManaged: { runId, timeoutMs } }`. Existing harnesses must read the nested `args` field on this path. A temporary Script named `DominusTestDeadline_<runId>` is installed in ServerScriptService. Its server copy checks Studio context and the matching run ID before scheduling `EndTest` at the deadline. The edit-mode original is removed when the engine invocation returns, including startup failures. An editor crash or failed cleanup may leave a dormant helper; `helperName` and `cleanupRequired` identify the relevant artifact/state. No helper is installed in the user's game by this development/test pass.

To locate runtime connections, call `studio_test_session` with `action: "discover"`, the originating edit `connectionId`, and the managed `runId`. Discovery first asks the origin to confirm that run, skips terminal runs, then probes up to 16 same-place connections (also matching the universe when known) in batches of four. Each probe has a five-second bridge deadline. It returns only non-edit connections reporting the exact run ID; it never switches the active target. The result includes scanned, failed, unidentified, and truncated counts. Clients without a run ID are unidentified, not guessed matches. A disconnected match is removed before returning; revalidate context on the next operation because connections and test state can change afterward.

To stop, use a discovered server connection with `action: "stop"` and the managed run ID. `stop-requested` means the server accepted `EndTest`; continue checking the originating run until terminal. Wrong-context stops fail explicitly. If no runtime server connection is exposed by the plugin lifecycle, manual Studio stop or the server deadline is still necessary. An empty discovery result is not proof that launch failed. Live discovery of a positive managed runtime remains unverified with the currently loaded older plugins.

Roblox documents `EndTest` as a server-DataModel operation and `LeaveTest` as a client disconnection operation. The prior legacy timeout's use of `LeaveTest` was removed. Managed tests use the server helper; legacy blocking `studio_run_test` still relies on its game's harness to end the session. [StudioTestService reference](https://create.roblox.com/docs/reference/engine/classes/StudioTestService/AddPlayers).

`RunService:Stop()` is deliberately not used: Roblox documents that simulation changes can persist afterward. [RunService Stop reference](https://create.roblox.com/docs/reference/engine/classes/RunService/Stop).

The latest ten managed run records are retained in plugin memory. Results are bounded for inline reporting; completion is distinct from assertions passing. If the engine has not returned after the observation deadline, status becomes `timeout-pending` with cleanup required, and another launch remains blocked. Transport tests verify explicit routing without active-target changes and MCP validation. They do not prove server-helper execution, readiness, stop behavior, or cleanup in Studio. The public SDK now exposes tests.start/status/stop/context through the same validated service as MCP. The control-panel session UI remains outstanding.

### Offline lifecycle coverage

`pnpm test:plugin` executes the production TestRunner module and its embedded deadline Script using the pinned Lune runtime with simulated Studio services and deterministic time. Fifteen cases cover run/play/multiplayer completion, argument wrapping, startup failures, session exclusion, deadline termination, timeout-pending state, server/run identity checks, stale deadlines, EndTest failures, cleanup failures, result encoding, and invalid starts. Helper creation failures now release the active session, and serialization failures preserve terminal status and cleanup evidence. These tests do not establish live Studio behavior across DataModels or plugin reloads.

## Incremental SDK observations

The daemon retains separate output journals for authenticated Studio connections. `studio.output.read()` returns an initial snapshot; pass its `nextCursor` into the next read to retrieve new observations. Drain `hasMore` pages before waiting for additional activity. Cursors are rejected for another connection or after reconnect.

```ts
let page = await studio.output.read({ level: 'error' });
console.log(page.entries);
page = await studio.output.read({ cursor: page.nextCursor, level: 'error' });
console.log(page.entries, page.missedEntries);
```

The initial snapshot selects the latest `limit` received events before severity filtering. Subsequent reads advance across nonmatching entries, so changing the severity filter does not replay earlier messages. Each connection retains at most 500 entries and 256 KiB of message text. Messages are capped at 8 KiB without splitting UTF-8; `truncated` identifies shortened messages. Entry data is bounded to approximately 64 KiB per page. `missedEntries` counts journal entries evicted since a cursor; it cannot count events never delivered by Studio.

Attribution comes from the authenticated socket, not an event-supplied connection ID. Journals are in memory, start at connection, and are discarded at disconnect. They do not import earlier plugin history or establish per-player/run attribution. `receivedAt` is daemon wall-clock time; plugin `timestamp` retains its original clock semantics.

`studio_get_output` now uses the same journal through an authenticated controller request when the daemon advertises support. Pass the returned `nextCursor` as `cursor`, keep the same selected Studio, and drain `hasMore` pages. This path does not send a new command to the Studio plugin. Older daemons retain the previous recent-output snapshot behavior; requesting a cursor on that path fails explicitly. Controller reconnects renegotiate support, and the server validates the explicit target and stream cursor.

## Scene readiness and QA assertions

`studio_check_scene` checks an explicit connection without changing the selected Studio. Supply up to 20 labelled targets and up to 32 distinct property names across the batch. Each target can require an exact class and property expectations using `equals`, `atLeast`, or `atMost`. Targets must resolve; unresolved targets and missing properties never count as passing. Enum equality uses the complete encoded string, such as `Enum.Material.Plastic`; vector values use numeric arrays.

```json
{
  "connectionId": "the-explicit-Studio-connection-UUID",
  "waitMs": 10000,
  "intervalMs": 500,
  "consecutivePasses": 2,
  "checks": [
    {
      "label": "Loading screen hidden",
      "target": { "pathSegments": ["Players", "YourPlayer", "PlayerGui", "LoadingGui"] },
      "className": "ScreenGui",
      "properties": { "Enabled": { "op": "equals", "value": false } }
    }
  ]
}
```

Use the matching runtime connection for gameplay checks and edit mode for build checks. Set `context: {role: "server", runId: "your-managed-run-UUID"}` to require a specific managed server run, or use role `client` with the same run ID for client UI observations. The updated plugin includes its test context in the same inspection response; each repeated observation must match. Missing context, wrong roles, or different run IDs stop immediately with `context-mismatch` and no passing assertions.

The managed server helper publishes `RomcpManagedRunId` on ReplicatedStorage only in the runtime DataModel, after validating its test arguments. Client context reads that marker. Discovery can match clients once replication arrives; before then they remain unidentified. Retry discovery instead of dropping the run-ID requirement. Launch rejects an existing attribute with that reserved name in the edit place and does not overwrite it. Runtime publication errors are warned without disabling the server deadline. Runtime teardown discards the marker; the helper never saves it into the edit place. This marker is test coordination metadata, not an authentication boundary against game scripts that can alter attributes.

This follows Roblox's [attribute replication model](https://create.roblox.com/docs/scripting/attributes). Lune tests execute the production helper and context reader with simulated services, covering delayed visibility, runtime disposal, reserved-name collisions, malformed markers, and publication failure with deadline preservation. Actual server-to-client replication and multiplayer plugin discovery still require live Studio validation.

`waitMs: 0` performs one observation; positive waits are capped at 30 seconds. Repeated observations happen inside the tool, with only the final checks returned to the model. Consecutive observations reduce transient matches but do not prove uninterrupted state between samples. A passed result proves only the supplied conditions, not overall gameplay quality. Read/transport errors stop the wait and are distinct from failed assertions.

The updated plugin's inspection path supports a property allowlist and omission of child references. The checker uses these to avoid transferring entire instance property sets and rejects Source expectations. Older plugins may still return their full inspection payload. Live UI/input sequencing, cross-DataModel readiness, and rendering fidelity remain release gates.

`pnpm test:scene` exercises the production Properties module in Lune with simulated instances. It verifies requested-property reads, omission of children, rejection of Source requests, and existence-only observations. Service tests cover bounded waits, consecutive passes, unresolved targets, missing properties, and failed observations. These checks do not establish live Studio readiness.

On September 7, read-only Workspace name/class checks passed through the live bridge in the connected Greedy Bees and Place1 windows (Studio 0.737). Both loaded plugins rejected the managed-test-context command and did not supply context in inspection responses. The new guard correctly returned `context-mismatch` on each rather than claiming server readiness. No scene mutations, play launches, active-target switches, or plugin reloads were performed. Updated-plugin positive runtime/run-ID validation still needs a live session.

## Input batches

### QA scenarios in one model call

`studio_run_qa_scenario` combines 2–8 labelled steps on an explicit `connectionId` and managed `runId`. Steps use `type: "check"` with a `scene` object accepted by `studio_check_scene`, or `type: "input"` with the same `actions` accepted by `studio_send_input`. The first and final steps must be checks. Omit `scene.context`: every check requires the same client role and run ID automatically, and each input uses the guarded v3 command.

All step schemas and input timing budgets are validated before dispatch. The default overall deadline is 60 seconds, configurable from 5 to 90 seconds, and each request is limited by the remaining time. The scenario stops on a failed assertion, changed runtime, failed/partial input, missing success evidence, or an expired deadline. It returns completed and skipped step counts plus per-step evidence. Check `data.passed`; successful MCP execution alone is not a passing scenario.

This saves model round trips; internal Studio requests and assertion polling still occur. The tool neither starts nor ends playtests, and does not switch the selected Studio. Completed input is not rolled back. A disconnected or timed-out input request has an unknown outcome and is never retried automatically; the Studio batch may still be running. Inspect that runtime before repeating input. A passing scenario proves only its supplied assertions, not visual fidelity or overall gameplay correctness. Automated fixtures cover ordering, context rejection before input, partial cleanup failures, unknown outcomes, and deadline handling; live scenario sequencing remains a release check.

For a managed playtest, supply both `connectionId` (the discovered client) and `runId` to `studio_send_input`. The guarded v3 command checks that runtime identity before obtaining VirtualInput and again before every action. A wrong role, missing marker, or changed run stops the batch; held inputs still receive best-effort release attempts. Older plugins reject this distinct command instead of silently ignoring its guard. Supplying a run ID without an explicit connection is rejected before dispatch. Explicit targeting never changes the active Studio selection. Calls without a run ID retain the legacy input behavior.

An identity change during a wait is detected before the next action; an action already in progress cannot be undone. This does not create atomicity between input and scene checks. `pnpm test:input` executes the production input module with simulated runtime changes, verifying rejection before delivery, held-key cleanup after a transition, and subsequent batch recovery. Hardware delivery and teardown races remain live Studio checks.

- Wait and hold durations may total at most 30 seconds. MCP rejects excessive batches before dispatch, and the plugin checks the budget independently.
- The plugin validates every action before delivering input, including key/button resolution, finite coordinates, text size, and durations.
- Concurrent batches in the same plugin process are rejected. The input loop tracks completed actions and attempts to release every key/button still held by the batch after both success and failure.
- Failures expose `applied`, `cleanupErrors`, and elapsed time through MCP. Completed input actions are not proof that gameplay assertions passed.
- Holds are scoped to a batch. Use a single down/wait/up sequence instead of relying on a key remaining down between tool calls.

Release attempts are best effort: engine errors, a frozen Studio process, process termination, and disconnected sessions can prevent immediate cleanup. No remote cancellation protocol or cross-client input coordinator has been implemented yet. The transport retains its 60-second timeout; timing out the host request does not itself cancel engine work.

The tests exercise timing-budget rejection before dispatch and preservation of partial-failure evidence. The Lua source is parser-checked and packaged, but held-input delivery/release and concurrency still require live Studio testing. Existing `studio_run_test` lifecycle limitations are not resolved by this change.
