Skip to content

feat: Add --expect to pause-point-status and clarify capture diagnostics - #2017

Merged
hatayama merged 13 commits into
feature/cli-discoverability-integrationfrom
feat/pause-point-status-expect
Jul 26, 2026
Merged

feat: Add --expect to pause-point-status and clarify capture diagnostics#2017
hatayama merged 13 commits into
feature/cli-discoverability-integrationfrom
feat/pause-point-status-expect

Conversation

@hatayama

@hatayama hatayama commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • pause-point-status now accepts --expect, so a marker's captured variables can be asserted from a plain status query, not only from await-pause-point.
  • A pause-point --trigger that Unity refused to run is now reported at the top level of the response instead of being buried several levels deep, and misplaced pause-point flags now name the command that actually owns them.

User Impact

Asserting captured variables (--expect)

  • Before: --expect existed only on await-pause-point. Once a marker had already been hit, re-checking its variables meant reading CapturedVariables by eye.
  • After: pause-point-status --id <id> --expect "Name=value" returns the same Expectations / AllExpectationsPassed fields the await path returns. Failing expectations do not change the exit code — whether the status query succeeded and whether the values matched stay separate.

A trigger that never ran

  • Before: arming a marker on a line that runs every frame and passing --trigger "simulate-keyboard --action Press --key W" produced Success: true / Status: Hit, while the triggered command had in fact been refused by Unity for running against a paused PlayMode — no input reached the game at all. The refusal was only visible inside TriggerResult.Response, and its cause was only in a message string.
  • After: the response carries top-level TriggerFailed: true whenever the trigger is known to have failed, plus a warning explaining that the marker was hit before the trigger ran, that this hit is not evidence about the trigger's input, and what to do instead (hold the key with a separate KeyDown before arming, or move the marker past the line where the input is applied). Unity's refusal now also carries the responsible pause point's id as a field, so the CLI can tell "refused by the marker I am waiting on" from "paused for some unrelated reason" without matching message text.
  • Both await-pause-point --trigger and enable-pause-point --await --trigger report this. They previously built their hit payloads in two separate places, so both now go through one shared builder — a field added for one command can no longer stay silently missing from the other.

Misplaced and unknown options

  • Before: any unrecognized option on await-pause-point / pause-point-status claimed the installed project runner might be older than the docs. For the misuse that actually happens — pause-point-status --max-preview-elements, a real enable-pause-point option — that hint pointed at the wrong cause entirely.
  • After: an option that belongs to another pause-point command says so (--max-preview-elements is an enable-pause-point option, not a pause-point-status one.), and for options whose enable-pause-point value already carries into later responses it adds that the value does not need passing again. An option that exists nowhere is treated as a typo and just points at --help.

Captured-variable names that matched nothing

  • Before: --captured-variable-names "isHeld,Health" silently returned only what matched; a typo looked the same as a variable that was genuinely out of scope.
  • After: the response lists the requested names that matched nothing as CapturedVariableNamesNotFound, in request order. The existing CapturedVariableNameFilterNoMatch flag is unchanged.

Changes

  • Go (project runner): --expect parsing and evaluation for pause-point-status, evaluated before the name/mode filters can strip values; a status-side wrapper type carrying Expectations / AllExpectationsPassed under the same names the await path uses.
  • Go: one buildPausePointHitPayload helper now shapes both hit paths; TriggerFailed and the refusal diagnosis live in one place; the warning join drops empty and repeated entries, and no longer lets an outer Warning field shadow — and discard — Unity's own text.
  • Go: unknown-option errors resolve the owning command deterministically from the shared flag sets (CLI-only plus schema-derived), replacing the unconditional stale-runner hint.
  • C#: a dedicated PlayModeToolPreflightResult return type for PlayModeToolPreflightService (valid / message / refusing pause-point id), with all five tools that share this preflight — simulate-keyboard, simulate-mouse-input, simulate-mouse-ui, record-input, replay-input — returning RejectedByActivePausePointId on a pause-point refusal. The generic argument-validation result type is untouched.

Known limitation

When a marker is hit so fast that the triggered command's response misses the join grace window, the trigger reports neither success nor failure (Completed: false, empty response). No diagnosis is emitted in that case: treating "unfinished" as failure would wrongly warn about long-running hold-style triggers.

Verification

  • sh scripts/check-go-cli.sh — format, vet, lint (0 issues. for every module) and all Go tests pass.
  • go test ./internal/projectrunner/ -run TestRunPausePointWaitAfterEnable -v — the three new enable-path tests execute and pass (verified in the -v output, not inferred).
  • uloop compile — ErrorCount 0, WarningCount 0.
  • uloop run-tests (EditMode, PlayModeToolPreflight|PausePointRejectionResponseField) — 19/19 pass.
  • Live run on a repository harness scene: enable-pause-point --file ... --line 16 --await --trigger "simulate-keyboard --action Press --key W" returns Status: Hit / Success: true with top-level TriggerFailed: true, the refusal warning, and TriggerResult.Response.RejectedByActivePausePointId set to the awaited marker's id.
  • Live runs also confirmed: pause-point-status --id <id> --expect pass/fail/not-found with exit code 0 alongside --captured-variables names; CapturedVariableNamesNotFound listing only the unmatched names; and the three unknown-option message shapes.
  • Pre-existing failures, present identically on the base branch (verified by running the same filter on a detached checkout of the base commit, 10/12 both times): SimulateMouseInputTests.Click_WhenPausePointMarkerHits_Should_ReturnMarkerDetails and Click_WhenUnityPausesDuringObservation_Should_CompleteAsPausePointInterruption. Neither the mouse-input implementation nor its tests are touched by this PR.
  • Repository CI does not trigger on pull requests targeting this integration branch, so the checks above were run locally; the integration branch's own PR will exercise CI.

Review in cubic

hatayama added 8 commits July 27, 2026 01:37
A hit can be inspected either by awaiting it or by querying it later, but only
await-pause-point could assert on the captured values. Callers that queried an
already-recorded hit had to re-read CapturedVariables by hand.

pause-point-status now accepts --expect and reports Expectations /
AllExpectationsPassed under the same field names await-pause-point uses, so one
response shape works for both commands. Expectations are evaluated before the
--captured-variable-names filter and the --captured-variables mode run, since
those can narrow the response or strip values that an expectation targets. A
failed expectation leaves the exit code at 0: whether the query succeeded and
whether the state matched are separate questions.

Also guards both runner-owned commands against help/parser drift: every flag
their --help advertises is now asserted to be accepted by their own parser.
Passing an enable-pause-point flag to a query command used to be answered with
"the installed project runner may be older than the docs — update the CLI".
That hint fired exactly when it was false: the flag does exist in this build,
just on another command. The one case it was written for (documentation ahead of
the binary) is not reachable in practice, because the package and the runner are
pinned together and each runner-owned command renders its own --help from the
same table its parser reads.

Unknown flags are now split in two. A flag another pause-point command accepts
names that command, and an enable-time setting whose value Unity reports back on
every later response also says it does not need to be passed again. A flag that
exists nowhere is reported as a plain unknown option pointing at --help.

The owning command is resolved through a fixed search order rather than map
iteration, so a flag several commands accept always reports the same owner.
--captured-variable-names only ever showed the names that did match, so a typo
in one of several names looked identical to a clean partial capture: the caller
had to diff the flag value against the response by hand.
CapturedVariableNamesNotFound now lists the requested names that matched no
captured variable, in the order they were requested.

CapturedVariableNameFilterNoMatch keeps its exact meaning and stays in place, so
nothing that reads it changes; both fields appear when nothing matched at all. A
name matched in any history frame counts as found, matching what the filter
itself considers a match.

The shared status-response contract fixture is deliberately left alone: like
CapturedVariableNameFilterNoMatch and TriggerResult, this field is produced by
the CLI, and the fixture pins the Unity-side contract only.
A simulate or record call made while a pause point holds PlayMode paused is
refused before it does anything, and until now the only trace of which pause
point caused that was inside the message text. Callers that need to tell
"refused by the marker I am waiting on" from "refused for some other reason" had
no option but to match on the wording — and PausePointId stays null here,
because that field reports a marker hit *during* the call, not a refusal.

Every response that shares this preflight — simulate-keyboard,
simulate-mouse-input, simulate-mouse-ui, record-input and replay-input — now
carries RejectedByActivePausePointId. The preflight returns its own result type
instead of the general-purpose ValidationResult, which has no place to put an id
and is used by argument validation that must not grow the field.

The paused branches are unreachable from EditMode tests, since PlayMode never
runs there, so the decision is split into Evaluate(isPlaying, isPaused,
activePausePointId, ...) with the editor-state read left in the thin wrapper.
Both the Unity status response and the CLI's hit payload expose a field named
Warning, and the CLI's outer field shadows the embedded Unity one. Any hit where
the CLI had something to say therefore silently discarded Unity's enable-time
diagnostic — the physics-callback dispatch warning among them.

The outer Warning is now the join of Unity's text and the CLI's, the same
treatment the enable path already gives it. The failed-log-fetch branch gained a
Warning field for the same reason: it had none at all, so Unity's warning was the
only one that could ever appear there, and only by accident of shadowing.
A marker armed on a line that runs every frame is hit before --trigger's input
reaches Unity, so the trigger is refused for running while PlayMode is paused and
injects nothing. The wait still reports Success:true and Status:Hit, with the
refusal three levels down in TriggerResult.Response — where a reader looking at
the top-level verdict never sees it, and the hit passes for input-driven
evidence it is not.

TriggerFailed is now promoted to the top level whenever a completed trigger
reports Success:false or its dispatch failed, and a refusal by the very marker
being awaited also produces a warning explaining how to arm the marker so the
input is actually in effect. The id must match the awaited marker: a PlayMode
paused by something else is a different problem.

InterruptedByPausePoint is deliberately not part of the predicate. It marks the
working case — the marker hit while the input was being applied — so an earlier
draft that keyed on it warned about exactly the runs that behaved correctly.

Known gap: a trigger that has not reported back within the join grace window has
no known outcome, so it is neither warned about nor marked failed. Treating an
unfinished trigger as failed would misreport every long-running hold.
Unity generated these after the test scripts were added; without them the
scripts have no stable GUID for other checkouts.
The trigger diagnosis (TriggerFailed plus the refusal warning) was wired only
into await-pause-point, while enable-pause-point --await builds its hit payload
in a second place. A real run of the documented form
`enable-pause-point --await --trigger "simulate-keyboard ..."` therefore still
reported a plain success with no warning, which is the exact scenario the
diagnosis exists for.

Rather than duplicating the wiring, both hit paths now build their payload
through one buildPausePointHitPayload helper, so a field added for one command
cannot silently stay missing from the other. The warning join also drops
repeats, because the same Unity text can reach the payload from both the enable
response and the status poll that observed the hit.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de6921d7-6085-4300-b1f1-7e4b8c80fcbb

📥 Commits

Reviewing files that changed from the base of the PR and between 964972b and 48bdaaf.

📒 Files selected for processing (7)
  • .agents/skills/uloop-pause-point/SKILL.md
  • .claude/skills/uloop-pause-point/SKILL.md
  • Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md
  • Packages/src/Editor/FirstPartyTools/Common/Preflight/PlayModeToolPreflightService.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationValidator.cs
  • cli/project-runner/internal/projectrunner/pause_point_captured_variable_names_filter_test.go
  • cli/project-runner/internal/projectrunner/pause_point_trigger_diagnosis_test.go

📝 Walkthrough

Walkthrough

The change adds structured pause-point rejection metadata to PlayMode tool responses and tests, while extending the project runner with expectation reporting, captured-variable diagnostics, deterministic option ownership, trigger diagnosis, warning composition, and unified awaited pause-point payload construction.

Changes

PlayMode rejection metadata

Layer / File(s) Summary
Preflight result evaluation
Packages/src/Editor/FirstPartyTools/Common/Preflight/*, Assets/Tests/Editor/PlayModeToolPreflight*Tests.cs
Preflight checks return structured results, including active pause-point identifiers for pause-point-specific failures.
Response rejection propagation
Packages/src/Editor/FirstPartyTools/{RecordInput,ReplayInput,SimulateKeyboard,SimulateMouseInput,SimulateMouseUi}/*, Assets/Tests/Editor/PausePointRejectionResponseFieldTests.cs
Input-tool responses propagate and serialize RejectedByActivePausePointId.

Pause-point CLI behavior

Layer / File(s) Summary
CLI response contracts and filtering
cli/project-runner/internal/projectrunner/pause_point_{types,captured_variable_names_filter}*
Status responses report trigger failures and missing captured-variable names with ordered, de-duplicated filtering.
Status expectations
cli/project-runner/internal/projectrunner/pause_point_wait.go, pause_point_status_expect_test.go, native_command_help.go
pause-point-status parses and evaluates --expect before value stripping and name filtering, emits optional expectation fields, and advertises the option.
Pause-point option ownership
cli/project-runner/internal/projectrunner/pause_point_unknown_option*.go, pause_point_cli_options_contract_test.go, pause_point_wait_test.go
Unknown options resolve to deterministic owning commands, carried-over enable flags are identified, and help options are checked against parsers.
Await payload and trigger diagnosis
cli/project-runner/internal/projectrunner/pause_point_{enable,logs,trigger_diagnosis,wait}.go, related tests
Awaited payloads combine trigger outcomes, warnings, expectations, and matching logs while preserving relevant fields when log retrieval fails.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly highlights the main change: adding --expect to pause-point-status and improving related diagnostics.
Description check ✅ Passed The description accurately matches the implemented --expect support and the diagnostic improvements across pause-point commands.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pause-point-status-expect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Self-review follow-up. A name passed twice in --captured-variable-names was
listed twice in CapturedVariableNamesNotFound, reading as two separate missing
variables; the list answers which names have no value, not how often each was
asked for. Also pins what --expect reports for a marker that is armed but not
yet hit, since that is the state a polling caller queries most.
The owner sentence hardcoded "is an <owner> ... not a <command> one", so a
vowel-initial command in the second slot read as "not a await-pause-point one".
One helper now decides the article for both slots from the command name.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@cli/project-runner/internal/projectrunner/pause_point_captured_variable_names_filter_test.go`:
- Around line 107-113: Strengthen the test case for
filterPausePointCapturedVariablesByName by asserting that the sole
CapturedVariableNamesNotFound entry is exactly "shield", in addition to
verifying its length is one.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ad4ccf5-a0e7-422c-9e27-fb8e8b3c9e39

📥 Commits

Reviewing files that changed from the base of the PR and between f897090 and 964972b.

📒 Files selected for processing (5)
  • cli/project-runner/internal/projectrunner/pause_point_captured_variable_names_filter.go
  • cli/project-runner/internal/projectrunner/pause_point_captured_variable_names_filter_test.go
  • cli/project-runner/internal/projectrunner/pause_point_status_expect_test.go
  • cli/project-runner/internal/projectrunner/pause_point_unknown_option.go
  • cli/project-runner/internal/projectrunner/pause_point_unknown_option_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • cli/project-runner/internal/projectrunner/pause_point_captured_variable_names_filter.go
  • cli/project-runner/internal/projectrunner/pause_point_unknown_option.go
  • cli/project-runner/internal/projectrunner/pause_point_unknown_option_test.go
  • cli/project-runner/internal/projectrunner/pause_point_status_expect_test.go

Comment on lines +107 to +113
t.Run("a name requested twice is reported missing once", func(t *testing.T) {
result := filterPausePointCapturedVariablesByName(
baseResponse(), []string{"shield", "shield"})
if len(result.CapturedVariableNamesNotFound) != 1 {
t.Fatalf("expected the repeated name once: %#v", result.CapturedVariableNamesNotFound)
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the duplicated name, not only the count.

This would pass even if the function returned an incorrect single-element list. Assert that the value is exactly "shield".

Proposed assertion
-		if len(result.CapturedVariableNamesNotFound) != 1 {
+		if len(result.CapturedVariableNamesNotFound) != 1 ||
+			result.CapturedVariableNamesNotFound[0] != "shield" {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t.Run("a name requested twice is reported missing once", func(t *testing.T) {
result := filterPausePointCapturedVariablesByName(
baseResponse(), []string{"shield", "shield"})
if len(result.CapturedVariableNamesNotFound) != 1 {
t.Fatalf("expected the repeated name once: %#v", result.CapturedVariableNamesNotFound)
}
})
t.Run("a name requested twice is reported missing once", func(t *testing.T) {
result := filterPausePointCapturedVariablesByName(
baseResponse(), []string{"shield", "shield"})
if len(result.CapturedVariableNamesNotFound) != 1 ||
result.CapturedVariableNamesNotFound[0] != "shield" {
t.Fatalf("expected the repeated name once: %#v", result.CapturedVariableNamesNotFound)
}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cli/project-runner/internal/projectrunner/pause_point_captured_variable_names_filter_test.go`
around lines 107 - 113, Strengthen the test case for
filterPausePointCapturedVariablesByName by asserting that the sole
CapturedVariableNamesNotFound entry is exactly "shield", in addition to
verifying its length is one.

hatayama added 3 commits July 27, 2026 08:05
SKILL.md still said --expect was not available on pause-point-status, which this
branch makes false, and none of the new diagnostic fields were described. Covers
--expect on pause-point-status (including what an unhit marker reports and that
the verdict never moves the exit code), the top-level TriggerFailed and its
warning, CapturedVariableNamesNotFound, and RejectedByActivePausePointId on a
refused simulate call. Generated copies regenerated with skills install.
Both files stopped referencing any ToolContracts type when they moved to
PlayModeToolPreflightResult; the test file's copy was already removed.
The five subtests added for CapturedVariableNamesNotFound pushed
TestFilterPausePointCapturedVariablesByName to cyclomatic complexity 24 against
the repository maximum of 15, so they move to their own test function sharing a
hoisted fixture. The refusal test now also asserts the raw stdout contains
"TriggerFailed": true, since every existing assertion goes through a Go struct
and would survive a renamed json tag.
@hatayama
hatayama merged commit d4fcf16 into feature/cli-discoverability-integration Jul 26, 2026
1 of 2 checks passed
@hatayama
hatayama deleted the feat/pause-point-status-expect branch July 26, 2026 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant